v3.6.0: Улучшено модальное окно переноса задачи - нередактируемое поле с понятным форматированием даты
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 55s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 55s
This commit is contained in:
@@ -3791,6 +3791,7 @@ func main() {
|
||||
protected.HandleFunc("/api/tasks/{id}", app.updateTaskHandler).Methods("PUT", "OPTIONS")
|
||||
protected.HandleFunc("/api/tasks/{id}", app.deleteTaskHandler).Methods("DELETE", "OPTIONS")
|
||||
protected.HandleFunc("/api/tasks/{id}/complete", app.completeTaskHandler).Methods("POST", "OPTIONS")
|
||||
protected.HandleFunc("/api/tasks/{id}/complete-and-delete", app.completeAndDeleteTaskHandler).Methods("POST", "OPTIONS")
|
||||
protected.HandleFunc("/api/tasks/{id}/postpone", app.postponeTaskHandler).Methods("POST", "OPTIONS")
|
||||
|
||||
// Admin operations
|
||||
@@ -7843,6 +7844,314 @@ func (a *App) completeTaskHandler(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// completeAndDeleteTaskHandler выполняет задачу и затем удаляет её
|
||||
func (a *App) completeAndDeleteTaskHandler(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
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
taskID, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
sendErrorWithCORS(w, "Invalid task ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Сначала выполняем задачу (используем ту же логику, что и в completeTaskHandler)
|
||||
// Создаем временный запрос для выполнения задачи
|
||||
var req CompleteTaskRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
log.Printf("Error decoding complete task request: %v", err)
|
||||
sendErrorWithCORS(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем задачу и проверяем владельца
|
||||
var task Task
|
||||
var rewardMessage sql.NullString
|
||||
var progressionBase sql.NullFloat64
|
||||
var repetitionPeriod sql.NullString
|
||||
var repetitionDate sql.NullString
|
||||
var ownerID int
|
||||
|
||||
err = a.DB.QueryRow(`
|
||||
SELECT id, name, reward_message, progression_base, repetition_period, repetition_date, user_id
|
||||
FROM tasks
|
||||
WHERE id = $1 AND deleted = FALSE
|
||||
`, taskID).Scan(&task.ID, &task.Name, &rewardMessage, &progressionBase, &repetitionPeriod, &repetitionDate, &ownerID)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
sendErrorWithCORS(w, "Task not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("Error querying task: %v", err)
|
||||
sendErrorWithCORS(w, fmt.Sprintf("Error querying task: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if ownerID != userID {
|
||||
sendErrorWithCORS(w, "Task not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Валидация: если progression_base != null, то value обязателен
|
||||
if progressionBase.Valid && req.Value == nil {
|
||||
sendErrorWithCORS(w, "Value is required when progression_base is set", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if rewardMessage.Valid {
|
||||
task.RewardMessage = &rewardMessage.String
|
||||
}
|
||||
if progressionBase.Valid {
|
||||
task.ProgressionBase = &progressionBase.Float64
|
||||
}
|
||||
|
||||
// Получаем награды основной задачи
|
||||
rewardRows, err := a.DB.Query(`
|
||||
SELECT rc.position, p.name AS project_name, rc.value, rc.use_progression
|
||||
FROM reward_configs rc
|
||||
JOIN projects p ON rc.project_id = p.id
|
||||
WHERE rc.task_id = $1
|
||||
ORDER BY rc.position
|
||||
`, taskID)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error querying rewards: %v", err)
|
||||
sendErrorWithCORS(w, fmt.Sprintf("Error querying rewards: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rewardRows.Close()
|
||||
|
||||
rewards := make([]Reward, 0)
|
||||
for rewardRows.Next() {
|
||||
var reward Reward
|
||||
err := rewardRows.Scan(&reward.Position, &reward.ProjectName, &reward.Value, &reward.UseProgression)
|
||||
if err != nil {
|
||||
log.Printf("Error scanning reward: %v", err)
|
||||
continue
|
||||
}
|
||||
rewards = append(rewards, reward)
|
||||
}
|
||||
|
||||
// Вычисляем score для каждой награды и формируем строки для подстановки
|
||||
rewardStrings := make(map[int]string)
|
||||
for _, reward := range rewards {
|
||||
var score float64
|
||||
if reward.UseProgression && progressionBase.Valid && req.Value != nil {
|
||||
score = (*req.Value / progressionBase.Float64) * reward.Value
|
||||
} else {
|
||||
score = reward.Value
|
||||
}
|
||||
|
||||
var rewardStr string
|
||||
if score >= 0 {
|
||||
rewardStr = fmt.Sprintf("**%s+%.4g**", reward.ProjectName, score)
|
||||
} else {
|
||||
rewardStr = fmt.Sprintf("**%s-%.4g**", reward.ProjectName, math.Abs(score))
|
||||
}
|
||||
rewardStrings[reward.Position] = rewardStr
|
||||
}
|
||||
|
||||
// Функция для замены плейсхолдеров в сообщении награды
|
||||
replaceRewardPlaceholders := func(message string, rewardStrings map[int]string) string {
|
||||
result := message
|
||||
escapedMarkers := make(map[string]string)
|
||||
for i := 0; i < 100; i++ {
|
||||
escaped := fmt.Sprintf(`\$%d`, i)
|
||||
marker := fmt.Sprintf(`__ESCAPED_DOLLAR_%d__`, i)
|
||||
if strings.Contains(result, escaped) {
|
||||
escapedMarkers[marker] = escaped
|
||||
result = strings.ReplaceAll(result, escaped, marker)
|
||||
}
|
||||
}
|
||||
for i := 0; i < 100; i++ {
|
||||
placeholder := fmt.Sprintf("${%d}", i)
|
||||
if rewardStr, ok := rewardStrings[i]; ok {
|
||||
result = strings.ReplaceAll(result, placeholder, rewardStr)
|
||||
}
|
||||
}
|
||||
for i := 99; i >= 0; i-- {
|
||||
if rewardStr, ok := rewardStrings[i]; ok {
|
||||
searchStr := fmt.Sprintf("$%d", i)
|
||||
for {
|
||||
idx := strings.LastIndex(result, searchStr)
|
||||
if idx == -1 {
|
||||
break
|
||||
}
|
||||
afterIdx := idx + len(searchStr)
|
||||
if afterIdx >= len(result) || result[afterIdx] < '0' || result[afterIdx] > '9' {
|
||||
result = result[:idx] + rewardStr + result[afterIdx:]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for marker, escaped := range escapedMarkers {
|
||||
result = strings.ReplaceAll(result, marker, escaped)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Подставляем в reward_message основной задачи
|
||||
var mainTaskMessage string
|
||||
if task.RewardMessage != nil && *task.RewardMessage != "" {
|
||||
mainTaskMessage = replaceRewardPlaceholders(*task.RewardMessage, rewardStrings)
|
||||
} else {
|
||||
mainTaskMessage = task.Name
|
||||
}
|
||||
|
||||
// Получаем выбранные подзадачи
|
||||
subtaskMessages := make([]string, 0)
|
||||
if len(req.ChildrenTaskIDs) > 0 {
|
||||
placeholders := make([]string, len(req.ChildrenTaskIDs))
|
||||
args := make([]interface{}, len(req.ChildrenTaskIDs)+1)
|
||||
args[0] = taskID
|
||||
for i, id := range req.ChildrenTaskIDs {
|
||||
placeholders[i] = fmt.Sprintf("$%d", i+2)
|
||||
args[i+1] = id
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
SELECT id, name, reward_message, progression_base
|
||||
FROM tasks
|
||||
WHERE parent_task_id = $1 AND id IN (%s) AND deleted = FALSE
|
||||
`, strings.Join(placeholders, ","))
|
||||
|
||||
subtaskRows, err := a.DB.Query(query, args...)
|
||||
if err != nil {
|
||||
log.Printf("Error querying subtasks: %v", err)
|
||||
} else {
|
||||
defer subtaskRows.Close()
|
||||
for subtaskRows.Next() {
|
||||
var subtaskID int
|
||||
var subtaskName string
|
||||
var subtaskRewardMessage sql.NullString
|
||||
var subtaskProgressionBase sql.NullFloat64
|
||||
|
||||
err := subtaskRows.Scan(&subtaskID, &subtaskName, &subtaskRewardMessage, &subtaskProgressionBase)
|
||||
if err != nil {
|
||||
log.Printf("Error scanning subtask: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if !subtaskRewardMessage.Valid || subtaskRewardMessage.String == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
subtaskRewardRows, err := a.DB.Query(`
|
||||
SELECT rc.position, p.name AS project_name, rc.value, rc.use_progression
|
||||
FROM reward_configs rc
|
||||
JOIN projects p ON rc.project_id = p.id
|
||||
WHERE rc.task_id = $1
|
||||
ORDER BY rc.position
|
||||
`, subtaskID)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error querying subtask rewards: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
subtaskRewards := make([]Reward, 0)
|
||||
for subtaskRewardRows.Next() {
|
||||
var reward Reward
|
||||
err := subtaskRewardRows.Scan(&reward.Position, &reward.ProjectName, &reward.Value, &reward.UseProgression)
|
||||
if err != nil {
|
||||
log.Printf("Error scanning subtask reward: %v", err)
|
||||
continue
|
||||
}
|
||||
subtaskRewards = append(subtaskRewards, reward)
|
||||
}
|
||||
subtaskRewardRows.Close()
|
||||
|
||||
subtaskRewardStrings := make(map[int]string)
|
||||
for _, reward := range subtaskRewards {
|
||||
var score float64
|
||||
if reward.UseProgression && subtaskProgressionBase.Valid && req.Value != nil {
|
||||
score = (*req.Value / subtaskProgressionBase.Float64) * reward.Value
|
||||
} else if reward.UseProgression && progressionBase.Valid && req.Value != nil {
|
||||
score = (*req.Value / progressionBase.Float64) * reward.Value
|
||||
} else {
|
||||
score = reward.Value
|
||||
}
|
||||
|
||||
var rewardStr string
|
||||
if score >= 0 {
|
||||
rewardStr = fmt.Sprintf("**%s+%.4g**", reward.ProjectName, score)
|
||||
} else {
|
||||
rewardStr = fmt.Sprintf("**%s-%.4g**", reward.ProjectName, math.Abs(score))
|
||||
}
|
||||
subtaskRewardStrings[reward.Position] = rewardStr
|
||||
}
|
||||
|
||||
subtaskMessage := replaceRewardPlaceholders(subtaskRewardMessage.String, subtaskRewardStrings)
|
||||
subtaskMessages = append(subtaskMessages, subtaskMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Формируем итоговое сообщение
|
||||
var finalMessage strings.Builder
|
||||
finalMessage.WriteString(mainTaskMessage)
|
||||
for _, subtaskMsg := range subtaskMessages {
|
||||
finalMessage.WriteString("\n + ")
|
||||
finalMessage.WriteString(subtaskMsg)
|
||||
}
|
||||
|
||||
// Отправляем сообщение через processMessage
|
||||
userIDPtr := &userID
|
||||
_, err = a.processMessage(finalMessage.String(), userIDPtr)
|
||||
if err != nil {
|
||||
log.Printf("Error sending message to Telegram: %v", err)
|
||||
}
|
||||
|
||||
// Обновляем выбранные подзадачи
|
||||
if len(req.ChildrenTaskIDs) > 0 {
|
||||
placeholders := make([]string, len(req.ChildrenTaskIDs))
|
||||
args := make([]interface{}, len(req.ChildrenTaskIDs))
|
||||
for i, id := range req.ChildrenTaskIDs {
|
||||
placeholders[i] = fmt.Sprintf("$%d", i+1)
|
||||
args[i] = id
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
UPDATE tasks
|
||||
SET completed = completed + 1, last_completed_at = NOW()
|
||||
WHERE id IN (%s) AND deleted = FALSE
|
||||
`, strings.Join(placeholders, ","))
|
||||
|
||||
_, err = a.DB.Exec(query, args...)
|
||||
if err != nil {
|
||||
log.Printf("Error updating subtasks completion: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Помечаем задачу как удаленную
|
||||
_, err = a.DB.Exec("UPDATE tasks SET deleted = TRUE WHERE id = $1 AND user_id = $2", taskID, userID)
|
||||
if err != nil {
|
||||
log.Printf("Error deleting task: %v", err)
|
||||
sendErrorWithCORS(w, fmt.Sprintf("Error deleting task: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "Task completed and deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// postponeTaskHandler переносит задачу на указанную дату
|
||||
func (a *App) postponeTaskHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "OPTIONS" {
|
||||
|
||||
Reference in New Issue
Block a user