4.6.0: Расчет срока разблокировки желаний
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m39s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m39s
This commit is contained in:
@@ -351,18 +351,19 @@ type LinkedTask struct {
|
||||
}
|
||||
|
||||
type WishlistItem struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Price *float64 `json:"price,omitempty"`
|
||||
ImageURL *string `json:"image_url,omitempty"`
|
||||
Link *string `json:"link,omitempty"`
|
||||
Unlocked bool `json:"unlocked"`
|
||||
Completed bool `json:"completed"`
|
||||
FirstLockedCondition *UnlockConditionDisplay `json:"first_locked_condition,omitempty"`
|
||||
MoreLockedConditions int `json:"more_locked_conditions,omitempty"`
|
||||
UnlockConditions []UnlockConditionDisplay `json:"unlock_conditions,omitempty"`
|
||||
LinkedTask *LinkedTask `json:"linked_task,omitempty"`
|
||||
TasksCount int `json:"tasks_count,omitempty"` // Количество задач для этого желания
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Price *float64 `json:"price,omitempty"`
|
||||
ImageURL *string `json:"image_url,omitempty"`
|
||||
Link *string `json:"link,omitempty"`
|
||||
Unlocked bool `json:"unlocked"`
|
||||
Completed bool `json:"completed"`
|
||||
FirstLockedCondition *UnlockConditionDisplay `json:"first_locked_condition,omitempty"`
|
||||
MoreLockedConditions int `json:"more_locked_conditions,omitempty"`
|
||||
LockedConditionsCount int `json:"locked_conditions_count,omitempty"` // Общее количество заблокированных условий
|
||||
UnlockConditions []UnlockConditionDisplay `json:"unlock_conditions,omitempty"`
|
||||
LinkedTask *LinkedTask `json:"linked_task,omitempty"`
|
||||
TasksCount int `json:"tasks_count,omitempty"` // Количество задач для этого желания
|
||||
}
|
||||
|
||||
type UnlockConditionDisplay struct {
|
||||
@@ -381,6 +382,8 @@ type UnlockConditionDisplay struct {
|
||||
// Персональные цели
|
||||
UserID *int `json:"user_id,omitempty"` // ID пользователя для персональных целей
|
||||
UserName *string `json:"user_name,omitempty"` // Имя пользователя для персональных целей
|
||||
// Срок разблокировки
|
||||
WeeksText *string `json:"weeks_text,omitempty"` // Отформатированный текст срока разблокировки
|
||||
}
|
||||
|
||||
type WishlistRequest struct {
|
||||
@@ -2812,7 +2815,7 @@ func (a *App) startWeeklyGoalsScheduler() {
|
||||
// Cron выражение: "0 6 * * 1" означает: минута=0, час=6, любой день месяца, любой месяц, понедельник (1)
|
||||
_, err = c.AddFunc("0 6 * * 1", func() {
|
||||
now := time.Now().In(loc)
|
||||
log.Printf("Scheduled task: Refreshing weekly report MV and setting up weekly goals (timezone: %s, local time: %s)", timezoneStr, now.Format("2006-01-02 15:04:05 MST"))
|
||||
log.Printf("Scheduled task: Refreshing materialized views and setting up weekly goals (timezone: %s, local time: %s)", timezoneStr, now.Format("2006-01-02 15:04:05 MST"))
|
||||
|
||||
// Сначала обновляем MV (чтобы в ней были данные прошлой недели)
|
||||
_, err := a.DB.Exec("REFRESH MATERIALIZED VIEW weekly_report_mv")
|
||||
@@ -2822,6 +2825,14 @@ func (a *App) startWeeklyGoalsScheduler() {
|
||||
log.Printf("Materialized view refreshed successfully")
|
||||
}
|
||||
|
||||
// Обновляем projects_median_mv после обновления weekly_report_mv
|
||||
_, err = a.DB.Exec("REFRESH MATERIALIZED VIEW projects_median_mv")
|
||||
if err != nil {
|
||||
log.Printf("Error refreshing projects_median_mv: %v", err)
|
||||
} else {
|
||||
log.Printf("Projects median materialized view refreshed successfully")
|
||||
}
|
||||
|
||||
// Затем настраиваем цели на новую неделю
|
||||
if err := a.setupWeeklyGoals(); err != nil {
|
||||
log.Printf("Error in scheduled weekly goals setup: %v", err)
|
||||
@@ -3713,6 +3724,7 @@ func main() {
|
||||
protected.HandleFunc("/api/wishlist/completed", app.getWishlistCompletedHandler).Methods("GET", "OPTIONS")
|
||||
protected.HandleFunc("/api/wishlist/metadata", app.extractLinkMetadataHandler).Methods("POST", "OPTIONS")
|
||||
protected.HandleFunc("/api/wishlist/proxy-image", app.proxyImageHandler).Methods("GET", "OPTIONS")
|
||||
protected.HandleFunc("/api/wishlist/calculate-weeks", app.calculateWeeksHandler).Methods("POST", "OPTIONS")
|
||||
|
||||
// Wishlist Boards (ВАЖНО: должны быть ПЕРЕД /api/wishlist/{id} чтобы избежать конфликта роутов!)
|
||||
protected.HandleFunc("/api/wishlist/boards", app.getBoardsHandler).Methods("GET", "OPTIONS")
|
||||
@@ -9165,6 +9177,104 @@ func (a *App) calculateProjectPointsFromDate(
|
||||
return totalScore, nil
|
||||
}
|
||||
|
||||
// getProjectMedian получает медиану проекта из materialized view projects_median_mv
|
||||
// Если медиана отсутствует, возвращает ошибку
|
||||
func (a *App) getProjectMedian(projectID int) (float64, error) {
|
||||
var median float64
|
||||
err := a.DB.QueryRow(`
|
||||
SELECT median_score
|
||||
FROM projects_median_mv
|
||||
WHERE project_id = $1
|
||||
`, projectID).Scan(&median)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return 0, fmt.Errorf("median not found for project %d", projectID)
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
return median, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// checkWishlistUnlock проверяет ВСЕ условия для желания
|
||||
// Все условия должны выполняться (AND логика)
|
||||
func (a *App) checkWishlistUnlock(itemID int, userID int) (bool, error) {
|
||||
@@ -9277,6 +9387,149 @@ func (a *App) checkWishlistUnlock(itemID int, userID int) (bool, error) {
|
||||
return allConditionsMet, nil
|
||||
}
|
||||
|
||||
// isConditionLocked определяет, заблокировано ли условие
|
||||
func isConditionLocked(cond UnlockConditionDisplay) bool {
|
||||
if cond.Type == "task_completion" {
|
||||
return cond.TaskCompleted == nil || !*cond.TaskCompleted
|
||||
} else if cond.Type == "project_points" {
|
||||
return cond.CurrentPoints == nil || cond.RequiredPoints == nil || *cond.CurrentPoints < *cond.RequiredPoints
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getConditionUnlockWeeks возвращает количество недель для разблокировки условия
|
||||
// Используется для сортировки заблокированных условий по баллам
|
||||
func (a *App) getConditionUnlockWeeks(cond UnlockConditionDisplay, userID int) float64 {
|
||||
if cond.Type != "project_points" {
|
||||
return 0
|
||||
}
|
||||
if cond.ProjectID == nil || cond.RequiredPoints == nil {
|
||||
return 99999.0
|
||||
}
|
||||
|
||||
var startDate sql.NullTime
|
||||
if cond.StartDate != nil {
|
||||
date, err := time.Parse("2006-01-02", *cond.StartDate)
|
||||
if err == nil {
|
||||
startDate = sql.NullTime{Time: date, Valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
conditionOwnerID := userID
|
||||
if cond.UserID != nil {
|
||||
conditionOwnerID = *cond.UserID
|
||||
}
|
||||
|
||||
return a.calculateProjectUnlockWeeks(*cond.ProjectID, *cond.RequiredPoints, startDate, conditionOwnerID)
|
||||
}
|
||||
|
||||
// sortUnlockConditions сортирует условия в следующем порядке:
|
||||
// 1. Заблокированные задачи (по алфавиту)
|
||||
// 2. Заблокированные баллы (по сроку от меньшего к большему)
|
||||
// 3. Разблокированные задачи (по алфавиту)
|
||||
// 4. Разблокированные баллы (по алфавиту)
|
||||
func (a *App) sortUnlockConditions(conditions []UnlockConditionDisplay, userID int) {
|
||||
sort.Slice(conditions, func(i, j int) bool {
|
||||
condI := conditions[i]
|
||||
condJ := conditions[j]
|
||||
|
||||
lockedI := isConditionLocked(condI)
|
||||
lockedJ := isConditionLocked(condJ)
|
||||
|
||||
// 1. Заблокированные идут перед разблокированными
|
||||
if lockedI != lockedJ {
|
||||
return lockedI // lockedI == true идет первым
|
||||
}
|
||||
|
||||
// Если оба заблокированы или оба разблокированы, сортируем по типу
|
||||
if lockedI {
|
||||
// Заблокированные: задачи идут перед баллами
|
||||
if condI.Type == "task_completion" && condJ.Type == "project_points" {
|
||||
return true
|
||||
}
|
||||
if condI.Type == "project_points" && condJ.Type == "task_completion" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Если оба одного типа
|
||||
if condI.Type == "task_completion" {
|
||||
// Заблокированные задачи: по алфавиту
|
||||
taskNameI := ""
|
||||
taskNameJ := ""
|
||||
if condI.TaskName != nil {
|
||||
taskNameI = *condI.TaskName
|
||||
}
|
||||
if condJ.TaskName != nil {
|
||||
taskNameJ = *condJ.TaskName
|
||||
}
|
||||
if taskNameI != taskNameJ {
|
||||
return taskNameI < taskNameJ
|
||||
}
|
||||
return condI.ID < condJ.ID
|
||||
} else {
|
||||
// Заблокированные баллы: по сроку от меньшего к большему
|
||||
weeksI := a.getConditionUnlockWeeks(condI, userID)
|
||||
weeksJ := a.getConditionUnlockWeeks(condJ, userID)
|
||||
if weeksI != weeksJ {
|
||||
return weeksI < weeksJ
|
||||
}
|
||||
// Если сроки равны, сортируем по алфавиту по названию проекта
|
||||
projectNameI := ""
|
||||
projectNameJ := ""
|
||||
if condI.ProjectName != nil {
|
||||
projectNameI = *condI.ProjectName
|
||||
}
|
||||
if condJ.ProjectName != nil {
|
||||
projectNameJ = *condJ.ProjectName
|
||||
}
|
||||
if projectNameI != projectNameJ {
|
||||
return projectNameI < projectNameJ
|
||||
}
|
||||
return condI.ID < condJ.ID
|
||||
}
|
||||
} else {
|
||||
// Разблокированные: задачи идут перед баллами
|
||||
if condI.Type == "task_completion" && condJ.Type == "project_points" {
|
||||
return true
|
||||
}
|
||||
if condI.Type == "project_points" && condJ.Type == "task_completion" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Если оба одного типа, сортируем по алфавиту
|
||||
if condI.Type == "task_completion" {
|
||||
// Разблокированные задачи: по алфавиту
|
||||
taskNameI := ""
|
||||
taskNameJ := ""
|
||||
if condI.TaskName != nil {
|
||||
taskNameI = *condI.TaskName
|
||||
}
|
||||
if condJ.TaskName != nil {
|
||||
taskNameJ = *condJ.TaskName
|
||||
}
|
||||
if taskNameI != taskNameJ {
|
||||
return taskNameI < taskNameJ
|
||||
}
|
||||
return condI.ID < condJ.ID
|
||||
} else {
|
||||
// Разблокированные баллы: по алфавиту
|
||||
projectNameI := ""
|
||||
projectNameJ := ""
|
||||
if condI.ProjectName != nil {
|
||||
projectNameI = *condI.ProjectName
|
||||
}
|
||||
if condJ.ProjectName != nil {
|
||||
projectNameJ = *condJ.ProjectName
|
||||
}
|
||||
if projectNameI != projectNameJ {
|
||||
return projectNameI < projectNameJ
|
||||
}
|
||||
return condI.ID < condJ.ID
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// getWishlistItemsWithConditions загружает желания с их условиями
|
||||
func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool) ([]WishlistItem, error) {
|
||||
query := `
|
||||
@@ -9424,6 +9677,9 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
|
||||
}
|
||||
item.Unlocked = unlocked
|
||||
|
||||
// Сортируем условия в нужном порядке
|
||||
a.sortUnlockConditions(item.UnlockConditions, userID)
|
||||
|
||||
// Определяем первое заблокированное условие и количество остальных, а также рассчитываем прогресс
|
||||
if !unlocked && !item.Completed {
|
||||
lockedCount := 0
|
||||
@@ -9489,6 +9745,7 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
|
||||
if firstLocked != nil {
|
||||
item.FirstLockedCondition = firstLocked
|
||||
item.MoreLockedConditions = lockedCount - 1
|
||||
item.LockedConditionsCount = lockedCount
|
||||
}
|
||||
} else {
|
||||
// Даже если желание разблокировано, рассчитываем прогресс для всех условий
|
||||
@@ -9533,6 +9790,17 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
|
||||
} else {
|
||||
condition.CurrentPoints = &totalScore
|
||||
}
|
||||
// Рассчитываем и форматируем срок разблокировки
|
||||
if condition.ProjectID != nil && condition.RequiredPoints != nil {
|
||||
weeks := a.calculateProjectUnlockWeeks(
|
||||
projectID,
|
||||
requiredPoints,
|
||||
startDate,
|
||||
conditionOwnerID,
|
||||
)
|
||||
weeksText := formatWeeksText(weeks)
|
||||
condition.WeeksText = &weeksText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9818,7 +10086,7 @@ func (a *App) getWishlistHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Сортируем внутри групп по цене (дорогие → дешёвые)
|
||||
// Сортируем разблокированные по цене от меньшего к большему
|
||||
sort.Slice(unlocked, func(i, j int) bool {
|
||||
priceI := 0.0
|
||||
priceJ := 0.0
|
||||
@@ -9828,21 +10096,53 @@ func (a *App) getWishlistHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if unlocked[j].Price != nil {
|
||||
priceJ = *unlocked[j].Price
|
||||
}
|
||||
return priceI > priceJ
|
||||
if priceI == priceJ {
|
||||
return unlocked[i].ID < unlocked[j].ID
|
||||
}
|
||||
return priceI < priceJ // Сортировка по цене от меньшего к большему (заменяет calculateUnlockedSortValue)
|
||||
})
|
||||
|
||||
sort.Slice(locked, func(i, j int) bool {
|
||||
priceI := 0.0
|
||||
priceJ := 0.0
|
||||
if locked[i].Price != nil {
|
||||
priceI = *locked[i].Price
|
||||
// Разделяем заблокированные на группы
|
||||
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 locked[j].Price != nil {
|
||||
priceJ = *locked[j].Price
|
||||
if hasUncompletedTasks {
|
||||
lockedWithTasks = append(lockedWithTasks, item)
|
||||
} else {
|
||||
lockedWithoutTasks = append(lockedWithoutTasks, item)
|
||||
}
|
||||
return priceI > priceJ
|
||||
}
|
||||
|
||||
// Сортируем каждую группу по времени разблокировки
|
||||
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...)
|
||||
|
||||
response := WishlistResponse{
|
||||
Unlocked: unlocked,
|
||||
Locked: locked,
|
||||
@@ -10056,6 +10356,62 @@ func (a *App) checkWishlistAccess(itemID int, userID int) (bool, int, sql.NullIn
|
||||
return hasAccess, itemUserID, boardID, nil
|
||||
}
|
||||
|
||||
// CalculateWeeksRequest структура запроса для расчета недель
|
||||
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"` // Владелец условия (если условие существует)
|
||||
}
|
||||
|
||||
// calculateWeeksHandler обрабатывает запрос на расчет недель для разблокировки условия
|
||||
func (a *App) calculateWeeksHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "OPTIONS" {
|
||||
setCORSHeaders(w)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
setCORSHeaders(w)
|
||||
|
||||
userID, ok := getUserIDFromContext(r)
|
||||
if !ok {
|
||||
sendErrorWithCORS(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var req CalculateWeeksRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
sendErrorWithCORS(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Определяем владельца условия:
|
||||
// 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), // Отформатированная строка для отображения
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// getWishlistItemHandler возвращает одно желание
|
||||
func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "OPTIONS" {
|
||||
@@ -10242,6 +10598,17 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) {
|
||||
dateStr := startDate.Time.Format("2006-01-02")
|
||||
condition.StartDate = &dateStr
|
||||
}
|
||||
// Рассчитываем и форматируем срок разблокировки
|
||||
if condition.ProjectID != nil && condition.RequiredPoints != nil {
|
||||
weeks := a.calculateProjectUnlockWeeks(
|
||||
*condition.ProjectID,
|
||||
*condition.RequiredPoints,
|
||||
startDate,
|
||||
conditionOwnerID,
|
||||
)
|
||||
weeksText := formatWeeksText(weeks)
|
||||
condition.WeeksText = &weeksText
|
||||
}
|
||||
}
|
||||
|
||||
item.UnlockConditions = append(item.UnlockConditions, condition)
|
||||
@@ -10286,6 +10653,9 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) {
|
||||
item.Unlocked = unlocked
|
||||
}
|
||||
|
||||
// Сортируем условия в нужном порядке
|
||||
a.sortUnlockConditions(item.UnlockConditions, userID)
|
||||
|
||||
// Загружаем связанную задачу текущего пользователя, если есть
|
||||
var linkedTaskID, linkedTaskCompleted, linkedTaskUserID sql.NullInt64
|
||||
var linkedTaskName sql.NullString
|
||||
@@ -10587,6 +10957,17 @@ func (a *App) updateWishlistHandler(w http.ResponseWriter, r *http.Request) {
|
||||
dateStr := startDate.Time.Format("2006-01-02")
|
||||
condition.StartDate = &dateStr
|
||||
}
|
||||
// Рассчитываем и форматируем срок разблокировки
|
||||
if condition.ProjectID != nil && condition.RequiredPoints != nil {
|
||||
weeks := a.calculateProjectUnlockWeeks(
|
||||
*condition.ProjectID,
|
||||
*condition.RequiredPoints,
|
||||
startDate,
|
||||
conditionOwnerID,
|
||||
)
|
||||
weeksText := formatWeeksText(weeks)
|
||||
condition.WeeksText = &weeksText
|
||||
}
|
||||
}
|
||||
|
||||
item.UnlockConditions = append(item.UnlockConditions, condition)
|
||||
@@ -10630,6 +11011,9 @@ func (a *App) updateWishlistHandler(w http.ResponseWriter, r *http.Request) {
|
||||
updatedItem.Unlocked = unlocked
|
||||
}
|
||||
|
||||
// Сортируем условия в нужном порядке
|
||||
a.sortUnlockConditions(updatedItem.UnlockConditions, userID)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(updatedItem)
|
||||
}
|
||||
@@ -12045,26 +12429,63 @@ func (a *App) getBoardItemsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Сортируем unlocked по возрастанию суммы баллов (от меньшего к большему)
|
||||
// Сортируем разблокированные по цене от меньшего к большему
|
||||
sort.Slice(unlocked, func(i, j int) bool {
|
||||
valueI := calculateUnlockedSortValue(unlocked[i])
|
||||
valueJ := calculateUnlockedSortValue(unlocked[j])
|
||||
if valueI == valueJ {
|
||||
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
|
||||
})
|
||||
|
||||
// Разделяем заблокированные на группы (с задачами и без задач)
|
||||
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
|
||||
})
|
||||
|
||||
// Сортируем locked по возрастанию суммы оставшихся баллов (от меньшего к большему)
|
||||
sort.Slice(locked, func(i, j int) bool {
|
||||
valueI := calculateLockedSortValue(locked[i])
|
||||
valueJ := calculateLockedSortValue(locked[j])
|
||||
sort.Slice(lockedWithTasks, func(i, j int) bool {
|
||||
valueI := a.calculateLockedSortValue(lockedWithTasks[i], userID)
|
||||
valueJ := a.calculateLockedSortValue(lockedWithTasks[j], userID)
|
||||
if valueI == valueJ {
|
||||
return locked[i].ID < locked[j].ID
|
||||
return lockedWithTasks[i].ID < lockedWithTasks[j].ID
|
||||
}
|
||||
return valueI < valueJ
|
||||
})
|
||||
|
||||
// Объединяем: сначала без задач, потом с задачами
|
||||
locked = append(lockedWithoutTasks, lockedWithTasks...)
|
||||
|
||||
// Считаем завершённые
|
||||
var completedCount int
|
||||
a.DB.QueryRow(`SELECT COUNT(*) FROM wishlist_items WHERE board_id = $1 AND completed = TRUE AND deleted = FALSE`,
|
||||
@@ -12313,27 +12734,77 @@ func calculateUnlockedSortValue(item WishlistItem) float64 {
|
||||
|
||||
// calculateLockedSortValue считает сумму оставшихся баллов для разблокировки
|
||||
// Задача считается как 1 балл (если не выполнена), project_points как remaining баллы
|
||||
func calculateLockedSortValue(item WishlistItem) float64 {
|
||||
var totalRemaining float64 = 0.0
|
||||
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 == "task_completion" {
|
||||
if condition.TaskCompleted == nil || !*condition.TaskCompleted {
|
||||
totalRemaining += 1.0
|
||||
}
|
||||
} else if condition.Type == "project_points" {
|
||||
if condition.CurrentPoints != nil && condition.RequiredPoints != nil {
|
||||
remaining := *condition.RequiredPoints - *condition.CurrentPoints
|
||||
if remaining > 0 {
|
||||
totalRemaining += remaining
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Если нет условий или все условия выполнены, ставим в конец
|
||||
if totalRemaining == 0.0 && len(item.UnlockConditions) > 0 {
|
||||
|
||||
// Если были условия по проектам, но все выполнены (maxWeeks = 0)
|
||||
if hasProjectConditions && maxWeeks == 0.0 {
|
||||
return 999999.0
|
||||
}
|
||||
return totalRemaining
|
||||
|
||||
// Если не было условий по проектам (только задачи или нет условий)
|
||||
if !hasProjectConditions {
|
||||
return 999999.0
|
||||
}
|
||||
|
||||
return maxWeeks
|
||||
}
|
||||
|
||||
// getWishlistItemsByBoard загружает желания конкретной доски
|
||||
@@ -12484,6 +12955,17 @@ func (a *App) getWishlistItemsByBoard(boardID int, userID int) ([]WishlistItem,
|
||||
dateStr := startDate.Time.Format("2006-01-02")
|
||||
condition.StartDate = &dateStr
|
||||
}
|
||||
// Рассчитываем и форматируем срок разблокировки
|
||||
if condition.ProjectID != nil && condition.RequiredPoints != nil {
|
||||
weeks := a.calculateProjectUnlockWeeks(
|
||||
*condition.ProjectID,
|
||||
*condition.RequiredPoints,
|
||||
startDate,
|
||||
conditionOwnerID,
|
||||
)
|
||||
weeksText := formatWeeksText(weeks)
|
||||
condition.WeeksText = &weeksText
|
||||
}
|
||||
}
|
||||
|
||||
item.UnlockConditions = append(item.UnlockConditions, condition)
|
||||
@@ -12493,6 +12975,9 @@ func (a *App) getWishlistItemsByBoard(boardID int, userID int) ([]WishlistItem,
|
||||
// Преобразуем map в slice и определяем unlocked
|
||||
items := make([]WishlistItem, 0, len(itemsMap))
|
||||
for _, item := range itemsMap {
|
||||
// Сортируем условия в нужном порядке
|
||||
a.sortUnlockConditions(item.UnlockConditions, userID)
|
||||
|
||||
// Проверяем все условия
|
||||
item.Unlocked = true
|
||||
if len(item.UnlockConditions) > 0 {
|
||||
@@ -12511,6 +12996,26 @@ func (a *App) getWishlistItemsByBoard(boardID int, userID int) ([]WishlistItem,
|
||||
}
|
||||
}
|
||||
|
||||
// Определяем первое заблокированное условие и количество остальных
|
||||
if !item.Unlocked && !item.Completed {
|
||||
lockedCount := 0
|
||||
var firstLocked *UnlockConditionDisplay
|
||||
for i := range item.UnlockConditions {
|
||||
condition := &item.UnlockConditions[i]
|
||||
if isConditionLocked(*condition) {
|
||||
lockedCount++
|
||||
if firstLocked == nil {
|
||||
firstLocked = condition
|
||||
}
|
||||
}
|
||||
}
|
||||
if firstLocked != nil {
|
||||
item.FirstLockedCondition = firstLocked
|
||||
item.MoreLockedConditions = lockedCount - 1
|
||||
item.LockedConditionsCount = lockedCount
|
||||
}
|
||||
}
|
||||
|
||||
// Загружаем связанную задачу текущего пользователя, если есть
|
||||
var linkedTaskID, linkedTaskCompleted, linkedTaskUserID sql.NullInt64
|
||||
var linkedTaskName sql.NullString
|
||||
|
||||
Reference in New Issue
Block a user