4.25.0: Группы вместо проектов для задач и желаний
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m31s

This commit is contained in:
poignatov
2026-02-06 17:42:36 +03:00
parent 0275d9aecf
commit 9f37d8b518
13 changed files with 888 additions and 188 deletions

View File

@@ -330,6 +330,7 @@ type Task struct {
Position *int `json:"position,omitempty"` // Position for subtasks
// Дополнительные поля для списка задач (без omitempty чтобы всегда передавались)
ProjectNames []string `json:"project_names"`
GroupName *string `json:"group_name,omitempty"` // Название группы задачи
SubtasksCount int `json:"subtasks_count"`
HasProgression bool `json:"has_progression"`
AutoComplete bool `json:"auto_complete"`
@@ -391,6 +392,7 @@ type TaskRequest struct {
RepetitionDate *string `json:"repetition_date,omitempty"`
WishlistID *int `json:"wishlist_id,omitempty"`
RewardPolicy *string `json:"reward_policy,omitempty"` // "personal" или "general" для задач, связанных с желаниями
GroupName *string `json:"group_name,omitempty"` // Название группы задачи
Rewards []RewardRequest `json:"rewards,omitempty"`
Subtasks []SubtaskRequest `json:"subtasks,omitempty"`
// Test-specific fields
@@ -464,9 +466,8 @@ type WishlistItem struct {
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"` // Количество задач для этого желания
ProjectID *int `json:"project_id,omitempty"` // ID проекта, к которому принадлежит желание
ProjectName *string `json:"project_name,omitempty"` // Название проекта
TasksCount int `json:"tasks_count,omitempty"` // Количество задач для этого желания
GroupName *string `json:"group_name,omitempty"` // Название группы желания
}
type UnlockConditionDisplay struct {
@@ -494,7 +495,7 @@ type WishlistRequest struct {
Name string `json:"name"`
Price *float64 `json:"price,omitempty"`
Link *string `json:"link,omitempty"`
ProjectID *int `json:"project_id,omitempty"` // ID проекта, к которому принадлежит желание
GroupName *string `json:"group_name,omitempty"` // Название группы желания
UnlockConditions []UnlockConditionRequest `json:"unlock_conditions,omitempty"`
}
@@ -4227,6 +4228,9 @@ func main() {
protected.HandleFunc("/api/wishlist/{id}/uncomplete", app.uncompleteWishlistHandler).Methods("POST", "OPTIONS")
protected.HandleFunc("/api/wishlist/{id}/copy", app.copyWishlistHandler).Methods("POST", "OPTIONS")
// Group suggestions
protected.HandleFunc("/api/group-suggestions", app.getGroupSuggestionsHandler).Methods("GET", "OPTIONS")
// Admin operations
protected.HandleFunc("/admin/recreate-mv", app.recreateMaterializedViewHandler).Methods("POST", "OPTIONS")
@@ -5868,6 +5872,11 @@ func (a *App) moveProjectHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Обновляем MV для групповых саджестов (имя проекта изменилось)
if err := a.refreshGroupSuggestionsMV(); err != nil {
log.Printf("Warning: Failed to refresh group suggestions MV: %v", err)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"message": "Project renamed successfully",
@@ -5949,8 +5958,10 @@ func (a *App) moveProjectHandler(w http.ResponseWriter, r *http.Request) {
return
}
// MV обновляется только по крону в понедельник в 6:00 утра
// Данные текущей недели берутся напрямую из nodes
// Обновляем MV для групповых саджестов (проект переименован или удалён)
if err := a.refreshGroupSuggestionsMV(); err != nil {
log.Printf("Warning: Failed to refresh group suggestions MV: %v", err)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
@@ -6027,8 +6038,10 @@ func (a *App) deleteProjectHandler(w http.ResponseWriter, r *http.Request) {
return
}
// MV обновляется только по крону в понедельник в 6:00 утра
// Данные текущей недели берутся напрямую из nodes
// Обновляем MV для групповых саджестов (проект удалён)
if err := a.refreshGroupSuggestionsMV(); err != nil {
log.Printf("Warning: Failed to refresh group suggestions MV: %v", err)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
@@ -6094,6 +6107,11 @@ func (a *App) createProjectHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Обновляем MV для групповых саджестов (проекты попадают в саджесты)
if err := a.refreshGroupSuggestionsMV(); err != nil {
log.Printf("Warning: Failed to refresh group suggestions MV: %v", err)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"message": "Project created successfully",
@@ -7333,6 +7351,7 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) {
t.wishlist_id,
t.config_id,
t.reward_policy,
t.group_name,
COALESCE((
SELECT COUNT(*)
FROM tasks st
@@ -7389,6 +7408,7 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) {
var wishlistID sql.NullInt64
var configID sql.NullInt64
var rewardPolicy sql.NullString
var groupName sql.NullString
var projectNames pq.StringArray
var subtaskProjectNames pq.StringArray
var autoComplete bool
@@ -7405,6 +7425,7 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) {
&wishlistID,
&configID,
&rewardPolicy,
&groupName,
&task.SubtasksCount,
&projectNames,
&subtaskProjectNames,
@@ -7444,6 +7465,10 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) {
if rewardPolicy.Valid {
task.RewardPolicy = &rewardPolicy.String
}
if groupName.Valid && groupName.String != "" {
groupNameVal := groupName.String
task.GroupName = &groupNameVal
}
task.AutoComplete = autoComplete
// Объединяем проекты из основной задачи и подзадач
@@ -7504,6 +7529,7 @@ func (a *App) getTaskDetailHandler(w http.ResponseWriter, r *http.Request) {
var wishlistID sql.NullInt64
var configID sql.NullInt64
var rewardPolicy sql.NullString
var groupName sql.NullString
// Сначала получаем значение как строку напрямую, чтобы избежать проблем с NULL
var repetitionPeriodStr string
@@ -7514,11 +7540,12 @@ func (a *App) getTaskDetailHandler(w http.ResponseWriter, r *http.Request) {
COALESCE(repetition_date, '') as repetition_date,
wishlist_id,
config_id,
reward_policy
reward_policy,
group_name
FROM tasks
WHERE id = $1 AND user_id = $2 AND deleted = FALSE
`, taskID, userID).Scan(
&task.ID, &task.Name, &task.Completed, &lastCompletedAt, &nextShowAt, &rewardMessage, &progressionBase, &repetitionPeriodStr, &repetitionDateStr, &wishlistID, &configID, &rewardPolicy,
&task.ID, &task.Name, &task.Completed, &lastCompletedAt, &nextShowAt, &rewardMessage, &progressionBase, &repetitionPeriodStr, &repetitionDateStr, &wishlistID, &configID, &rewardPolicy, &groupName,
)
log.Printf("Scanned repetition_period for task %d: String='%s', repetition_date='%s'", taskID, repetitionPeriodStr, repetitionDateStr)
@@ -7578,6 +7605,10 @@ func (a *App) getTaskDetailHandler(w http.ResponseWriter, r *http.Request) {
if rewardPolicy.Valid {
task.RewardPolicy = &rewardPolicy.String
}
if groupName.Valid && groupName.String != "" {
groupNameVal := groupName.String
task.GroupName = &groupNameVal
}
// Получаем награды основной задачи
rewards := make([]Reward, 0)
@@ -8111,11 +8142,11 @@ func (a *App) createTaskHandler(w http.ResponseWriter, r *http.Request) {
now := time.Now().In(loc)
insertSQL = `
INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, repetition_date, next_show_at, completed, deleted, wishlist_id, reward_policy)
VALUES ($1, $2, $3, $4, $5::INTERVAL, NULL, $6, 0, FALSE, $7, $8)
INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, repetition_date, next_show_at, completed, deleted, wishlist_id, reward_policy, group_name)
VALUES ($1, $2, $3, $4, $5::INTERVAL, NULL, $6, 0, FALSE, $7, $8, $9)
RETURNING id
`
insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionPeriodValue, now, wishlistIDValue, rewardPolicyValue}
insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionPeriodValue, now, wishlistIDValue, rewardPolicyValue, req.GroupName}
} else if repetitionDate.Valid {
// Вычисляем next_show_at для задачи с repetition_date
@@ -8130,26 +8161,26 @@ func (a *App) createTaskHandler(w http.ResponseWriter, r *http.Request) {
nextShowAt := calculateNextShowAtFromRepetitionDate(repetitionDate.String, time.Now().In(loc))
if nextShowAt != nil {
insertSQL = `
INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, repetition_date, next_show_at, completed, deleted, wishlist_id, reward_policy)
VALUES ($1, $2, $3, $4, NULL, $5, $6, 0, FALSE, $7, $8)
RETURNING id
`
insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionDate.String, nextShowAt, wishlistIDValue, rewardPolicyValue}
INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, repetition_date, next_show_at, completed, deleted, wishlist_id, reward_policy, group_name)
VALUES ($1, $2, $3, $4, NULL, $5, $6, 0, FALSE, $7, $8, $9)
RETURNING id
`
insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionDate.String, nextShowAt, wishlistIDValue, rewardPolicyValue, req.GroupName}
} else {
insertSQL = `
INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, repetition_date, completed, deleted, wishlist_id, reward_policy)
VALUES ($1, $2, $3, $4, NULL, $5, 0, FALSE, $6, $7)
RETURNING id
`
insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionDate.String, wishlistIDValue, rewardPolicyValue}
INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, repetition_date, completed, deleted, wishlist_id, reward_policy, group_name)
VALUES ($1, $2, $3, $4, NULL, $5, 0, FALSE, $6, $7, $8)
RETURNING id
`
insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionDate.String, wishlistIDValue, rewardPolicyValue, req.GroupName}
}
} else {
insertSQL = `
INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, repetition_date, completed, deleted, wishlist_id, reward_policy)
VALUES ($1, $2, $3, $4, NULL, NULL, 0, FALSE, $5, $6)
INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, repetition_date, completed, deleted, wishlist_id, reward_policy, group_name)
VALUES ($1, $2, $3, $4, NULL, NULL, 0, FALSE, $5, $6, $7)
RETURNING id
`
insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, wishlistIDValue, rewardPolicyValue}
insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, wishlistIDValue, rewardPolicyValue, req.GroupName}
}
err = tx.QueryRow(insertSQL, insertArgs...).Scan(&taskID)
@@ -8316,6 +8347,13 @@ func (a *App) createTaskHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Обновляем MV для групповых саджестов
if req.GroupName != nil && *req.GroupName != "" {
if err := a.refreshGroupSuggestionsMV(); err != nil {
log.Printf("Warning: Failed to refresh group suggestions MV: %v", err)
}
}
// Возвращаем созданную задачу
var createdTask Task
var lastCompletedAt sql.NullString
@@ -8530,35 +8568,35 @@ func (a *App) updateTaskHandler(w http.ResponseWriter, r *http.Request) {
now := time.Now().In(loc)
updateSQL = `
UPDATE tasks
SET name = $1, reward_message = $2, progression_base = $3, repetition_period = $4::INTERVAL, repetition_date = NULL, next_show_at = $5, wishlist_id = $6, reward_policy = $7
WHERE id = $8
SET name = $1, reward_message = $2, progression_base = $3, repetition_period = $4::INTERVAL, repetition_date = NULL, next_show_at = $5, wishlist_id = $6, reward_policy = $7, group_name = $8
WHERE id = $9
`
updateArgs = []interface{}{strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionPeriod.String, now, newWishlistID, rewardPolicyValue, taskID}
updateArgs = []interface{}{strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionPeriod.String, now, newWishlistID, rewardPolicyValue, req.GroupName, taskID}
} else if repetitionDate.Valid {
// Вычисляем next_show_at для задачи с repetition_date
nextShowAt := calculateNextShowAtFromRepetitionDate(repetitionDate.String, time.Now().In(loc))
if nextShowAt != nil {
updateSQL = `
UPDATE tasks
SET name = $1, reward_message = $2, progression_base = $3, repetition_period = NULL, repetition_date = $4, next_show_at = $5, wishlist_id = $6, reward_policy = $7
WHERE id = $8
`
updateArgs = []interface{}{strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionDate.String, nextShowAt, newWishlistID, rewardPolicyValue, taskID}
UPDATE tasks
SET name = $1, reward_message = $2, progression_base = $3, repetition_period = NULL, repetition_date = $4, next_show_at = $5, wishlist_id = $6, reward_policy = $7, group_name = $8
WHERE id = $9
`
updateArgs = []interface{}{strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionDate.String, nextShowAt, newWishlistID, rewardPolicyValue, req.GroupName, taskID}
} else {
updateSQL = `
UPDATE tasks
SET name = $1, reward_message = $2, progression_base = $3, repetition_period = NULL, repetition_date = $4, wishlist_id = $5, reward_policy = $6
WHERE id = $7
`
updateArgs = []interface{}{strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionDate.String, newWishlistID, rewardPolicyValue, taskID}
UPDATE tasks
SET name = $1, reward_message = $2, progression_base = $3, repetition_period = NULL, repetition_date = $4, wishlist_id = $5, reward_policy = $6, group_name = $7
WHERE id = $8
`
updateArgs = []interface{}{strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionDate.String, newWishlistID, rewardPolicyValue, req.GroupName, taskID}
}
} else {
updateSQL = `
UPDATE tasks
SET name = $1, reward_message = $2, progression_base = $3, repetition_period = NULL, repetition_date = NULL, next_show_at = NULL, wishlist_id = $4, reward_policy = $5
WHERE id = $6
SET name = $1, reward_message = $2, progression_base = $3, repetition_period = NULL, repetition_date = NULL, next_show_at = NULL, wishlist_id = $4, reward_policy = $5, group_name = $6
WHERE id = $7
`
updateArgs = []interface{}{strings.TrimSpace(req.Name), rewardMessage, progressionBase, newWishlistID, rewardPolicyValue, taskID}
updateArgs = []interface{}{strings.TrimSpace(req.Name), rewardMessage, progressionBase, newWishlistID, rewardPolicyValue, req.GroupName, taskID}
}
_, err = tx.Exec(updateSQL, updateArgs...)
@@ -8877,6 +8915,13 @@ func (a *App) updateTaskHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Обновляем MV для групповых саджестов
if req.GroupName != nil && *req.GroupName != "" {
if err := a.refreshGroupSuggestionsMV(); err != nil {
log.Printf("Warning: Failed to refresh group suggestions MV: %v", err)
}
}
// Возвращаем обновленную задачу
var updatedTask Task
var lastCompletedAt sql.NullString
@@ -10347,8 +10392,7 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
wi.image_path,
wi.link,
wi.completed,
wi.project_id AS item_project_id,
wp.name AS item_project_name,
wi.group_name,
wc.id AS condition_id,
wc.display_order,
wc.task_condition_id,
@@ -10361,7 +10405,6 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
sc.required_points,
sc.start_date
FROM wishlist_items wi
LEFT JOIN projects wp ON wi.project_id = wp.id AND wp.deleted = FALSE
LEFT JOIN wishlist_conditions wc ON wi.id = wc.wishlist_item_id
LEFT JOIN task_conditions tc ON wc.task_condition_id = tc.id
LEFT JOIN tasks t ON tc.task_id = t.id AND t.deleted = FALSE
@@ -10388,8 +10431,7 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
var price sql.NullFloat64
var imagePath, link sql.NullString
var completed bool
var itemProjectID sql.NullInt64
var itemProjectName sql.NullString
var groupName sql.NullString
var conditionID, displayOrder sql.NullInt64
var taskConditionID, scoreConditionID sql.NullInt64
@@ -10403,7 +10445,7 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
err := rows.Scan(
&itemID, &name, &price, &imagePath, &link, &completed,
&itemProjectID, &itemProjectName,
&groupName,
&conditionID, &displayOrder,
&taskConditionID, &scoreConditionID, &conditionUserID,
&taskID, &taskName,
@@ -10434,13 +10476,9 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
l := link.String
item.Link = &l
}
if itemProjectID.Valid {
projectIDVal := int(itemProjectID.Int64)
item.ProjectID = &projectIDVal
}
if itemProjectName.Valid {
projectNameVal := itemProjectName.String
item.ProjectName = &projectNameVal
if groupName.Valid && groupName.String != "" {
groupNameVal := groupName.String
item.GroupName = &groupNameVal
}
itemsMap[itemID] = item
}
@@ -11108,10 +11146,10 @@ func (a *App) createWishlistHandler(w http.ResponseWriter, r *http.Request) {
var wishlistID int
err = tx.QueryRow(`
INSERT INTO wishlist_items (user_id, author_id, name, price, link, project_id, completed, deleted)
INSERT INTO wishlist_items (user_id, author_id, name, price, link, group_name, completed, deleted)
VALUES ($1, $1, $2, $3, $4, $5, FALSE, FALSE)
RETURNING id
`, userID, strings.TrimSpace(req.Name), req.Price, req.Link, req.ProjectID).Scan(&wishlistID)
`, userID, strings.TrimSpace(req.Name), req.Price, req.Link, req.GroupName).Scan(&wishlistID)
if err != nil {
log.Printf("Error creating wishlist item: %v", err)
@@ -11141,6 +11179,13 @@ func (a *App) createWishlistHandler(w http.ResponseWriter, r *http.Request) {
}
log.Printf("createWishlistHandler: transaction committed successfully")
// Обновляем MV для групповых саджестов
if req.GroupName != nil && *req.GroupName != "" {
if err := a.refreshGroupSuggestionsMV(); err != nil {
log.Printf("Warning: Failed to refresh group suggestions MV: %v", err)
}
}
// Получаем созданное желание с условиями
items, err := a.getWishlistItemsWithConditions(userID, false)
if err != nil {
@@ -11322,8 +11367,7 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) {
wi.image_path,
wi.link,
wi.completed,
wi.project_id AS item_project_id,
wp.name AS item_project_name,
wi.group_name,
wc.id AS condition_id,
wc.display_order,
wc.task_condition_id,
@@ -11337,7 +11381,6 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) {
sc.required_points,
sc.start_date
FROM wishlist_items wi
LEFT JOIN projects wp ON wi.project_id = wp.id AND wp.deleted = FALSE
LEFT JOIN wishlist_conditions wc ON wi.id = wc.wishlist_item_id
LEFT JOIN task_conditions tc ON wc.task_condition_id = tc.id
LEFT JOIN tasks t ON tc.task_id = t.id AND t.deleted = FALSE
@@ -11364,8 +11407,7 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) {
var imagePath sql.NullString
var link sql.NullString
var completed bool
var itemProjectID sql.NullInt64
var itemProjectName sql.NullString
var groupName sql.NullString
var conditionID sql.NullInt64
var displayOrder sql.NullInt64
var taskConditionID sql.NullInt64
@@ -11380,7 +11422,7 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) {
var startDate sql.NullTime
err := rows.Scan(
&itemID, &name, &price, &imagePath, &link, &completed, &itemProjectID, &itemProjectName,
&itemID, &name, &price, &imagePath, &link, &completed, &groupName,
&conditionID, &displayOrder, &taskConditionID, &scoreConditionID, &conditionUserID,
&taskID, &taskName, &taskNextShowAt, &projectID, &projectName, &requiredPoints, &startDate,
)
@@ -11410,13 +11452,9 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) {
if link.Valid {
item.Link = &link.String
}
if itemProjectID.Valid {
projectIDVal := int(itemProjectID.Int64)
item.ProjectID = &projectIDVal
}
if itemProjectName.Valid {
projectNameVal := itemProjectName.String
item.ProjectName = &projectNameVal
if groupName.Valid && groupName.String != "" {
groupNameVal := groupName.String
item.GroupName = &groupNameVal
}
itemsMap[itemID] = item
}
@@ -11679,9 +11717,9 @@ func (a *App) updateWishlistHandler(w http.ResponseWriter, r *http.Request) {
// Обновляем желание (не проверяем user_id в WHERE, так как доступ уже проверен выше)
_, err = tx.Exec(`
UPDATE wishlist_items
SET name = $1, price = $2, link = $3, project_id = $4, updated_at = NOW()
SET name = $1, price = $2, link = $3, group_name = $4, updated_at = NOW()
WHERE id = $5
`, strings.TrimSpace(req.Name), req.Price, req.Link, req.ProjectID, itemID)
`, strings.TrimSpace(req.Name), req.Price, req.Link, req.GroupName, itemID)
if err != nil {
log.Printf("Error updating wishlist item: %v", err)
@@ -11703,6 +11741,13 @@ func (a *App) updateWishlistHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Обновляем MV для групповых саджестов
if req.GroupName != nil && *req.GroupName != "" {
if err := a.refreshGroupSuggestionsMV(); err != nil {
log.Printf("Warning: Failed to refresh group suggestions MV: %v", err)
}
}
// Получаем обновлённое желание через getWishlistItemHandler логику
// Используем тот же запрос, что и в getWishlistItemHandler
query := `
@@ -11713,6 +11758,7 @@ func (a *App) updateWishlistHandler(w http.ResponseWriter, r *http.Request) {
wi.image_path,
wi.link,
wi.completed,
wi.group_name,
wc.id AS condition_id,
wc.display_order,
wc.task_condition_id,
@@ -11752,6 +11798,7 @@ func (a *App) updateWishlistHandler(w http.ResponseWriter, r *http.Request) {
var imagePath sql.NullString
var link sql.NullString
var completed bool
var groupName sql.NullString
var conditionID sql.NullInt64
var displayOrder sql.NullInt64
var taskConditionID sql.NullInt64
@@ -11765,7 +11812,7 @@ func (a *App) updateWishlistHandler(w http.ResponseWriter, r *http.Request) {
var startDate sql.NullTime
err := rows.Scan(
&itemID, &name, &price, &imagePath, &link, &completed,
&itemID, &name, &price, &imagePath, &link, &completed, &groupName,
&conditionID, &displayOrder, &taskConditionID, &scoreConditionID, &conditionUserID,
&taskID, &taskName, &projectID, &projectName, &requiredPoints, &startDate,
)
@@ -11802,6 +11849,10 @@ func (a *App) updateWishlistHandler(w http.ResponseWriter, r *http.Request) {
if link.Valid {
item.Link = &link.String
}
if groupName.Valid && groupName.String != "" {
groupNameVal := groupName.String
item.GroupName = &groupNameVal
}
itemsMap[itemID] = item
}
@@ -12416,11 +12467,12 @@ func (a *App) copyWishlistHandler(w http.ResponseWriter, r *http.Request) {
var ownerID int
var boardID sql.NullInt64
var authorID sql.NullInt64
var groupName sql.NullString
err = a.DB.QueryRow(`
SELECT user_id, name, price, link, image_path, board_id, author_id
SELECT user_id, name, price, link, image_path, board_id, author_id, group_name
FROM wishlist_items
WHERE id = $1 AND deleted = FALSE
`, itemID).Scan(&ownerID, &name, &price, &link, &imagePath, &boardID, &authorID)
`, itemID).Scan(&ownerID, &name, &price, &link, &imagePath, &boardID, &authorID, &groupName)
if err == sql.ErrNoRows || ownerID != userID {
sendErrorWithCORS(w, "Wishlist item not found", http.StatusNotFound)
@@ -12512,7 +12564,7 @@ func (a *App) copyWishlistHandler(w http.ResponseWriter, r *http.Request) {
}
// Определяем значения для board_id и author_id
var boardIDVal, authorIDVal interface{}
var boardIDVal, authorIDVal, groupNameVal interface{}
if boardID.Valid {
boardIDVal = int(boardID.Int64)
}
@@ -12522,12 +12574,15 @@ func (a *App) copyWishlistHandler(w http.ResponseWriter, r *http.Request) {
// Если author_id не был установлен, используем текущего пользователя
authorIDVal = userID
}
if groupName.Valid {
groupNameVal = groupName.String
}
err = tx.QueryRow(`
INSERT INTO wishlist_items (user_id, board_id, author_id, name, price, link, completed, deleted)
VALUES ($1, $2, $3, $4, $5, $6, FALSE, FALSE)
INSERT INTO wishlist_items (user_id, board_id, author_id, name, price, link, group_name, completed, deleted)
VALUES ($1, $2, $3, $4, $5, $6, $7, FALSE, FALSE)
RETURNING id
`, ownerID, boardIDVal, authorIDVal, name+" (копия)", priceVal, linkVal).Scan(&newWishlistID)
`, ownerID, boardIDVal, authorIDVal, name+" (копия)", priceVal, linkVal, groupNameVal).Scan(&newWishlistID)
if err != nil {
log.Printf("Error creating wishlist copy: %v", err)
sendErrorWithCORS(w, fmt.Sprintf("Error creating wishlist copy: %v", err), http.StatusInternalServerError)
@@ -12615,6 +12670,13 @@ func (a *App) copyWishlistHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Обновляем MV для групповых саджестов
if groupName.Valid && groupName.String != "" {
if err := a.refreshGroupSuggestionsMV(); err != nil {
log.Printf("Warning: Failed to refresh group suggestions MV: %v", err)
}
}
// Получаем созданное желание с условиями
items, err := a.getWishlistItemsWithConditions(userID, false)
if err != nil {
@@ -13844,8 +13906,7 @@ func (a *App) getWishlistItemsByBoard(boardID int, userID int) ([]WishlistItem,
wi.image_path,
wi.link,
wi.completed,
wi.project_id AS item_project_id,
wp.name AS item_project_name,
wi.group_name,
COALESCE(wi.author_id, wi.user_id) AS item_owner_id,
wc.id AS condition_id,
wc.display_order,
@@ -13859,7 +13920,6 @@ func (a *App) getWishlistItemsByBoard(boardID int, userID int) ([]WishlistItem,
sc.required_points,
sc.start_date
FROM wishlist_items wi
LEFT JOIN projects wp ON wi.project_id = wp.id AND wp.deleted = FALSE
LEFT JOIN wishlist_conditions wc ON wi.id = wc.wishlist_item_id
LEFT JOIN task_conditions tc ON wc.task_condition_id = tc.id
LEFT JOIN tasks t ON tc.task_id = t.id AND t.deleted = FALSE
@@ -13886,8 +13946,7 @@ func (a *App) getWishlistItemsByBoard(boardID int, userID int) ([]WishlistItem,
var imagePath sql.NullString
var link sql.NullString
var completed bool
var itemProjectID sql.NullInt64
var itemProjectName sql.NullString
var groupName sql.NullString
var itemOwnerID sql.NullInt64
var conditionID sql.NullInt64
var displayOrder sql.NullInt64
@@ -13902,7 +13961,7 @@ func (a *App) getWishlistItemsByBoard(boardID int, userID int) ([]WishlistItem,
var startDate sql.NullTime
err := rows.Scan(
&itemID, &name, &price, &imagePath, &link, &completed, &itemProjectID, &itemProjectName, &itemOwnerID,
&itemID, &name, &price, &imagePath, &link, &completed, &groupName, &itemOwnerID,
&conditionID, &displayOrder, &taskConditionID, &scoreConditionID, &conditionUserID,
&taskID, &taskName, &projectID, &projectName, &requiredPoints, &startDate,
)
@@ -13932,13 +13991,9 @@ func (a *App) getWishlistItemsByBoard(boardID int, userID int) ([]WishlistItem,
if link.Valid {
item.Link = &link.String
}
if itemProjectID.Valid {
projectIDVal := int(itemProjectID.Int64)
item.ProjectID = &projectIDVal
}
if itemProjectName.Valid {
projectNameVal := itemProjectName.String
item.ProjectName = &projectNameVal
if groupName.Valid && groupName.String != "" {
groupNameVal := groupName.String
item.GroupName = &groupNameVal
}
itemsMap[itemID] = item
}
@@ -14211,10 +14266,10 @@ func (a *App) createBoardItemHandler(w http.ResponseWriter, r *http.Request) {
var itemID int
err = tx.QueryRow(`
INSERT INTO wishlist_items (user_id, board_id, author_id, name, price, link, project_id, completed, deleted)
INSERT INTO wishlist_items (user_id, board_id, author_id, name, price, link, group_name, completed, deleted)
VALUES ($1, $2, $3, $4, $5, $6, $7, FALSE, FALSE)
RETURNING id
`, ownerID, boardID, userID, strings.TrimSpace(req.Name), req.Price, req.Link, req.ProjectID).Scan(&itemID)
`, ownerID, boardID, userID, strings.TrimSpace(req.Name), req.Price, req.Link, req.GroupName).Scan(&itemID)
if err != nil {
log.Printf("createBoardItemHandler: Error creating board item: %v", err)
@@ -14244,6 +14299,13 @@ func (a *App) createBoardItemHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Обновляем MV для групповых саджестов
if req.GroupName != nil && *req.GroupName != "" {
if err := a.refreshGroupSuggestionsMV(); err != nil {
log.Printf("Warning: Failed to refresh group suggestions MV: %v", err)
}
}
// Возвращаем созданное желание
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
@@ -15521,3 +15583,57 @@ func decodeHTMLEntities(s string) string {
}
return s
}
// refreshGroupSuggestionsMV обновляет materialized view для групповых саджестов
func (a *App) refreshGroupSuggestionsMV() error {
_, err := a.DB.Exec("REFRESH MATERIALIZED VIEW CONCURRENTLY user_group_suggestions_mv")
if err != nil {
log.Printf("Error refreshing user_group_suggestions_mv: %v", err)
return err
}
return nil
}
// getGroupSuggestionsHandler возвращает список уникальных имён групп для текущего пользователя
func (a *App) getGroupSuggestionsHandler(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
}
query := `
SELECT DISTINCT group_name
FROM user_group_suggestions_mv
WHERE user_id = $1
ORDER BY group_name
`
rows, err := a.DB.Query(query, userID)
if err != nil {
log.Printf("Error querying group suggestions: %v", err)
sendErrorWithCORS(w, fmt.Sprintf("Error querying group suggestions: %v", err), http.StatusInternalServerError)
return
}
defer rows.Close()
groups := make([]string, 0)
for rows.Next() {
var groupName string
if err := rows.Scan(&groupName); err != nil {
log.Printf("Error scanning group name: %v", err)
continue
}
groups = append(groups, groupName)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(groups)
}