6.14.0: Еженедельное подтверждение приоритетов
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m19s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m19s
This commit is contained in:
@@ -169,6 +169,7 @@ type WeeklyStatsResponse struct {
|
||||
Projects []WeeklyProjectStats `json:"projects"`
|
||||
Wishes []WishlistItem `json:"wishes,omitempty"`
|
||||
PendingScoresByProject map[int]float64 `json:"pending_scores_by_project,omitempty"`
|
||||
PrioritiesConfirmed bool `json:"priorities_confirmed"`
|
||||
}
|
||||
|
||||
type MessagePostRequest struct {
|
||||
@@ -284,11 +285,13 @@ type TelegramUpdate struct {
|
||||
|
||||
// Tracking structures
|
||||
type TrackingUserStats struct {
|
||||
UserID int `json:"user_id"`
|
||||
UserName string `json:"user_name"`
|
||||
IsCurrentUser bool `json:"is_current_user"`
|
||||
Total *float64 `json:"total,omitempty"`
|
||||
Projects []TrackingProjectStats `json:"projects"`
|
||||
UserID int `json:"user_id"`
|
||||
UserName string `json:"user_name"`
|
||||
IsCurrentUser bool `json:"is_current_user"`
|
||||
Total *float64 `json:"total,omitempty"`
|
||||
Projects []TrackingProjectStats `json:"projects"`
|
||||
PrioritiesConfirmedYear int `json:"priorities_confirmed_year"`
|
||||
PrioritiesConfirmedWeek int `json:"priorities_confirmed_week"`
|
||||
}
|
||||
|
||||
type TrackingProjectStats struct {
|
||||
@@ -2997,6 +3000,13 @@ func (a *App) getWeeklyStatsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if pendingByProject == nil {
|
||||
pendingByProject = make(map[int]float64)
|
||||
}
|
||||
|
||||
confirmed, err := a.isPrioritiesConfirmedForUser(userID)
|
||||
if err != nil {
|
||||
log.Printf("Error checking priorities confirmation for user %d: %v", userID, err)
|
||||
confirmed = false
|
||||
}
|
||||
|
||||
response := WeeklyStatsResponse{
|
||||
Total: total,
|
||||
GroupProgress1: groupsProgress.Group1,
|
||||
@@ -3005,6 +3015,7 @@ func (a *App) getWeeklyStatsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
Projects: projects,
|
||||
Wishes: wishes,
|
||||
PendingScoresByProject: pendingByProject,
|
||||
PrioritiesConfirmed: confirmed,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -3220,6 +3231,8 @@ func (a *App) startWeeklyGoalsScheduler() {
|
||||
log.Printf("Project score sample materialized view refreshed successfully")
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Затем настраиваем цели на новую неделю
|
||||
if err := a.setupWeeklyGoals(); err != nil {
|
||||
log.Printf("Error in scheduled weekly goals setup: %v", err)
|
||||
@@ -3908,13 +3921,20 @@ func (a *App) getWeeklyStatsDataForUser(userID int) (*WeeklyStatsResponse, error
|
||||
// Фильтруем желания для экрана прогресса недели
|
||||
wishes := a.filterWishesForWeekProgress(allWishes, projectMinGoalScores)
|
||||
|
||||
confirmed, err := a.isPrioritiesConfirmedForUser(userID)
|
||||
if err != nil {
|
||||
log.Printf("Error checking priorities confirmation for user %d: %v", userID, err)
|
||||
confirmed = false
|
||||
}
|
||||
|
||||
response := WeeklyStatsResponse{
|
||||
Total: total,
|
||||
GroupProgress1: groupsProgress.Group1,
|
||||
GroupProgress2: groupsProgress.Group2,
|
||||
GroupProgress0: groupsProgress.Group0,
|
||||
Projects: projects,
|
||||
Wishes: wishes,
|
||||
Total: total,
|
||||
GroupProgress1: groupsProgress.Group1,
|
||||
GroupProgress2: groupsProgress.Group2,
|
||||
GroupProgress0: groupsProgress.Group0,
|
||||
Projects: projects,
|
||||
Wishes: wishes,
|
||||
PrioritiesConfirmed: confirmed,
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
@@ -3955,9 +3975,9 @@ func (a *App) formatDailyReport(data *WeeklyStatsResponse) string {
|
||||
}
|
||||
|
||||
// Форматирование текста целей
|
||||
// Проверяем, что minGoal валиден (не NaN, как в JS коде: !isNaN(minGoal))
|
||||
// Цели не отображаются пока пользователь не подтвердил приоритеты на этой неделе
|
||||
goalText := ""
|
||||
if !math.IsNaN(minGoal) {
|
||||
if data.PrioritiesConfirmed && !math.IsNaN(minGoal) {
|
||||
if hasMaxGoal && !math.IsNaN(maxGoal) {
|
||||
goalText = fmt.Sprintf(" (Цель: %.1f–%.1f)", minGoal, maxGoal)
|
||||
} else {
|
||||
@@ -4517,6 +4537,7 @@ func main() {
|
||||
// Note: /weekly_goals/setup, /daily-report/trigger moved to adminAPIRoutes
|
||||
protected.HandleFunc("/projects", app.getProjectsHandler).Methods("GET", "OPTIONS")
|
||||
protected.HandleFunc("/project/priority", app.setProjectPriorityHandler).Methods("POST", "OPTIONS")
|
||||
protected.HandleFunc("/priorities/confirm", app.confirmPrioritiesHandler).Methods("POST", "OPTIONS")
|
||||
protected.HandleFunc("/project/color", app.setProjectColorHandler).Methods("POST", "OPTIONS")
|
||||
protected.HandleFunc("/project/move", app.moveProjectHandler).Methods("POST", "OPTIONS")
|
||||
protected.HandleFunc("/project/delete", app.deleteProjectHandler).Methods("POST", "OPTIONS")
|
||||
@@ -5561,6 +5582,115 @@ func (a *App) setupWeeklyGoals() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// isPrioritiesConfirmedForUser проверяет, подтвердил ли пользователь приоритеты на текущей ISO-неделе
|
||||
func (a *App) isPrioritiesConfirmedForUser(userID int) (bool, error) {
|
||||
timezoneStr := getEnv("TIMEZONE", "UTC")
|
||||
loc, err := time.LoadLocation(timezoneStr)
|
||||
if err != nil {
|
||||
loc = time.UTC
|
||||
}
|
||||
now := time.Now().In(loc)
|
||||
currentYear, currentWeek := now.ISOWeek()
|
||||
|
||||
var confirmedYear, confirmedWeek int
|
||||
err = a.DB.QueryRow(
|
||||
`SELECT priorities_confirmed_year, priorities_confirmed_week FROM users WHERE id = $1`,
|
||||
userID,
|
||||
).Scan(&confirmedYear, &confirmedWeek)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return confirmedYear == currentYear && confirmedWeek == currentWeek, nil
|
||||
}
|
||||
|
||||
// setPrioritiesConfirmed помечает приоритеты пользователя как подтверждённые на текущей ISO-неделе
|
||||
func (a *App) setPrioritiesConfirmed(userID int) error {
|
||||
timezoneStr := getEnv("TIMEZONE", "UTC")
|
||||
loc, err := time.LoadLocation(timezoneStr)
|
||||
if err != nil {
|
||||
loc = time.UTC
|
||||
}
|
||||
now := time.Now().In(loc)
|
||||
currentYear, currentWeek := now.ISOWeek()
|
||||
|
||||
_, err = a.DB.Exec(
|
||||
`UPDATE users SET priorities_confirmed_year = $1, priorities_confirmed_week = $2 WHERE id = $3`,
|
||||
currentYear, currentWeek, userID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// setupWeeklyGoalsForUser устанавливает цели на текущую неделю для одного пользователя
|
||||
func (a *App) setupWeeklyGoalsForUser(userID int) error {
|
||||
setupQuery := `
|
||||
WITH current_info AS (
|
||||
SELECT
|
||||
EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER AS c_year,
|
||||
EXTRACT(WEEK FROM CURRENT_DATE)::INTEGER AS c_week
|
||||
),
|
||||
goal_metrics AS (
|
||||
SELECT
|
||||
project_id,
|
||||
AVG(normalized_total_score) AS avg_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
|
||||
WHERE rn <= 4
|
||||
GROUP BY project_id
|
||||
)
|
||||
INSERT INTO weekly_goals (
|
||||
project_id,
|
||||
goal_year,
|
||||
goal_week,
|
||||
min_goal_score,
|
||||
max_goal_score,
|
||||
priority,
|
||||
user_id
|
||||
)
|
||||
SELECT
|
||||
p.id,
|
||||
ci.c_year,
|
||||
ci.c_week,
|
||||
COALESCE(gm.avg_score, 0) AS min_goal_score,
|
||||
CASE
|
||||
WHEN gm.avg_score IS NULL THEN NULL
|
||||
WHEN p.priority = 1 THEN gm.avg_score * 2.0
|
||||
WHEN p.priority = 2 THEN gm.avg_score * 1.7
|
||||
ELSE gm.avg_score * 1.4
|
||||
END AS max_goal_score,
|
||||
p.priority,
|
||||
p.user_id
|
||||
FROM projects p
|
||||
CROSS JOIN current_info ci
|
||||
LEFT JOIN goal_metrics gm ON p.id = gm.project_id
|
||||
WHERE p.deleted = FALSE AND p.user_id = $1
|
||||
ON CONFLICT (project_id, goal_year, goal_week) DO UPDATE
|
||||
SET
|
||||
min_goal_score = EXCLUDED.min_goal_score,
|
||||
max_goal_score = EXCLUDED.max_goal_score,
|
||||
priority = EXCLUDED.priority,
|
||||
user_id = EXCLUDED.user_id
|
||||
`
|
||||
|
||||
_, err := a.DB.Exec(setupQuery, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting up weekly goals for user %d: %w", userID, err)
|
||||
}
|
||||
|
||||
log.Printf("Weekly goals setup completed for user %d", userID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// getWeeklyGoalsForUser получает цели для конкретного пользователя
|
||||
func (a *App) getWeeklyGoalsForUser(userID int) ([]WeeklyGoalSetup, error) {
|
||||
selectQuery := `
|
||||
@@ -6195,6 +6325,117 @@ func (a *App) setProjectPriorityHandler(w http.ResponseWriter, r *http.Request)
|
||||
})
|
||||
}
|
||||
|
||||
// confirmPrioritiesHandler сохраняет приоритеты и помечает их как подтверждённые на текущей неделе.
|
||||
// Если приоритеты ещё не подтверждались на этой неделе — также пересчитывает цели.
|
||||
func (a *App) confirmPrioritiesHandler(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
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
sendErrorWithCORS(w, "Error reading request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
var projectsToUpdate []ProjectPriorityUpdate
|
||||
|
||||
var directArray []interface{}
|
||||
arrayErr := json.Unmarshal(bodyBytes, &directArray)
|
||||
if arrayErr == nil && len(directArray) > 0 {
|
||||
for _, item := range directArray {
|
||||
if itemMap, ok := item.(map[string]interface{}); ok {
|
||||
var project ProjectPriorityUpdate
|
||||
if idVal, ok := itemMap["id"].(float64); ok {
|
||||
project.ID = int(idVal)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
if priorityVal, ok := itemMap["priority"]; ok && priorityVal != nil {
|
||||
if numVal, ok := priorityVal.(float64); ok {
|
||||
priorityInt := int(numVal)
|
||||
project.Priority = &priorityInt
|
||||
} else {
|
||||
project.Priority = nil
|
||||
}
|
||||
} else {
|
||||
project.Priority = nil
|
||||
}
|
||||
projectsToUpdate = append(projectsToUpdate, project)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(projectsToUpdate) == 0 {
|
||||
sendErrorWithCORS(w, "No projects to update", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем, подтверждены ли уже приоритеты на этой неделе
|
||||
alreadyConfirmed, err := a.isPrioritiesConfirmedForUser(userID)
|
||||
if err != nil {
|
||||
log.Printf("Error checking priorities confirmation for user %d: %v", userID, err)
|
||||
alreadyConfirmed = false
|
||||
}
|
||||
|
||||
// Сохраняем приоритеты в транзакции
|
||||
tx, err := a.DB.Begin()
|
||||
if err != nil {
|
||||
sendErrorWithCORS(w, "Error beginning transaction", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
for _, project := range projectsToUpdate {
|
||||
if project.Priority == nil {
|
||||
_, err = tx.Exec(`UPDATE projects SET priority = NULL WHERE id = $1 AND user_id = $2`, project.ID, userID)
|
||||
} else {
|
||||
_, err = tx.Exec(`UPDATE projects SET priority = $1 WHERE id = $2 AND user_id = $3`, *project.Priority, project.ID, userID)
|
||||
}
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
sendErrorWithCORS(w, fmt.Sprintf("Error updating project %d", project.ID), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
sendErrorWithCORS(w, "Error committing transaction", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Помечаем приоритеты как подтверждённые
|
||||
if err := a.setPrioritiesConfirmed(userID); err != nil {
|
||||
log.Printf("Error setting priorities confirmed for user %d: %v", userID, err)
|
||||
}
|
||||
|
||||
// Пересчитываем цели только если ещё не подтверждали на этой неделе
|
||||
goalsUpdated := false
|
||||
if !alreadyConfirmed {
|
||||
if err := a.setupWeeklyGoalsForUser(userID); err != nil {
|
||||
log.Printf("Error setting up weekly goals for user %d: %v", userID, err)
|
||||
} else {
|
||||
goalsUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"message": "Priorities confirmed",
|
||||
"goals_updated": goalsUpdated,
|
||||
})
|
||||
}
|
||||
|
||||
type ProjectColorRequest struct {
|
||||
ID int `json:"id"`
|
||||
Color string `json:"color"`
|
||||
@@ -17466,12 +17707,23 @@ func (a *App) getTrackingStatsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return pi < pj
|
||||
})
|
||||
|
||||
var confirmedYear, confirmedWeek int
|
||||
err = a.DB.QueryRow(
|
||||
`SELECT priorities_confirmed_year, priorities_confirmed_week FROM users WHERE id = $1`,
|
||||
uid,
|
||||
).Scan(&confirmedYear, &confirmedWeek)
|
||||
if err != nil {
|
||||
log.Printf("Error getting priorities confirmed for user %d: %v", uid, err)
|
||||
}
|
||||
|
||||
users = append(users, TrackingUserStats{
|
||||
UserID: uid,
|
||||
UserName: userName,
|
||||
IsCurrentUser: uid == userID,
|
||||
Total: stats.Total,
|
||||
Projects: projects,
|
||||
UserID: uid,
|
||||
UserName: userName,
|
||||
IsCurrentUser: uid == userID,
|
||||
Total: stats.Total,
|
||||
Projects: projects,
|
||||
PrioritiesConfirmedYear: confirmedYear,
|
||||
PrioritiesConfirmedWeek: confirmedWeek,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE users
|
||||
DROP COLUMN IF EXISTS priorities_confirmed_year,
|
||||
DROP COLUMN IF EXISTS priorities_confirmed_week;
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE users
|
||||
ADD COLUMN priorities_confirmed_year INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN priorities_confirmed_week INTEGER NOT NULL DEFAULT 0;
|
||||
Reference in New Issue
Block a user