4.14.0: Добавлен дневной прирост в карточках
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m25s

This commit is contained in:
poignatov
2026-02-04 15:04:58 +03:00
parent 8023319ee4
commit e66a3cecce
4 changed files with 135 additions and 5 deletions

View File

@@ -1 +1 @@
4.13.6 4.14.0

View File

@@ -112,6 +112,7 @@ type WeeklyProjectStats struct {
MaxGoalScore *float64 `json:"max_goal_score,omitempty"` MaxGoalScore *float64 `json:"max_goal_score,omitempty"`
Priority *int `json:"priority,omitempty"` Priority *int `json:"priority,omitempty"`
CalculatedScore float64 `json:"calculated_score"` CalculatedScore float64 `json:"calculated_score"`
TodayChange *float64 `json:"today_change,omitempty"`
} }
type GroupsProgress struct { type GroupsProgress struct {
@@ -2536,6 +2537,14 @@ func (a *App) getWeeklyStatsHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
// Получаем сегодняшние приросты
todayScores, err := a.getTodayScores(userID)
if err != nil {
log.Printf("Error getting today scores: %v", err)
sendErrorWithCORS(w, err.Error(), http.StatusInternalServerError)
return
}
query := ` query := `
SELECT SELECT
p.id AS project_id, p.id AS project_id,
@@ -2600,6 +2609,11 @@ func (a *App) getWeeklyStatsHandler(w http.ResponseWriter, r *http.Request) {
project.TotalScore = currentWeekScore project.TotalScore = currentWeekScore
} }
// Добавляем сегодняшний прирост
if todayScore, exists := todayScores[projectID]; exists && todayScore != 0 {
project.TodayChange = &todayScore
}
if minGoalScore.Valid { if minGoalScore.Valid {
project.MinGoalScore = minGoalScore.Float64 project.MinGoalScore = minGoalScore.Float64
} else { } else {
@@ -2939,6 +2953,80 @@ func (a *App) getCurrentWeekScores(userID int) (map[int]float64, error) {
return scores, nil return scores, nil
} }
// getTodayScores получает сумму score всех нод, созданных сегодня для конкретного пользователя
// Возвращает map[project_id]today_score для сегодняшнего дня
func (a *App) getTodayScores(userID int) (map[int]float64, error) {
query := `
SELECT
n.project_id,
COALESCE(SUM(n.score), 0) AS today_score
FROM nodes n
JOIN projects p ON n.project_id = p.id
WHERE
p.deleted = FALSE
AND p.user_id = $1
AND n.user_id = $1
AND DATE(n.created_date) = CURRENT_DATE
GROUP BY n.project_id
`
rows, err := a.DB.Query(query, userID)
if err != nil {
log.Printf("Error querying today scores: %v", err)
return nil, fmt.Errorf("error querying today scores: %w", err)
}
defer rows.Close()
scores := make(map[int]float64)
for rows.Next() {
var projectID int
var todayScore float64
if err := rows.Scan(&projectID, &todayScore); err != nil {
log.Printf("Error scanning today scores row: %v", err)
return nil, fmt.Errorf("error scanning today scores row: %w", err)
}
scores[projectID] = todayScore
}
return scores, nil
}
// getTodayScoresAllUsers получает сумму score всех нод, созданных сегодня для всех пользователей
// Возвращает map[project_id]today_score для сегодняшнего дня
func (a *App) getTodayScoresAllUsers() (map[int]float64, error) {
query := `
SELECT
n.project_id,
COALESCE(SUM(n.score), 0) AS today_score
FROM nodes n
JOIN projects p ON n.project_id = p.id
WHERE
p.deleted = FALSE
AND DATE(n.created_date) = CURRENT_DATE
GROUP BY n.project_id
`
rows, err := a.DB.Query(query)
if err != nil {
log.Printf("Error querying today scores for all users: %v", err)
return nil, fmt.Errorf("error querying today scores for all users: %w", err)
}
defer rows.Close()
scores := make(map[int]float64)
for rows.Next() {
var projectID int
var todayScore float64
if err := rows.Scan(&projectID, &todayScore); err != nil {
log.Printf("Error scanning today scores row: %v", err)
return nil, fmt.Errorf("error scanning today scores row: %w", err)
}
scores[projectID] = todayScore
}
return scores, nil
}
// getCurrentWeekScoresAllUsers получает данные текущей недели для всех пользователей // getCurrentWeekScoresAllUsers получает данные текущей недели для всех пользователей
// Возвращает map[project_id]total_score для текущей недели // Возвращает map[project_id]total_score для текущей недели
func (a *App) getCurrentWeekScoresAllUsers() (map[int]float64, error) { func (a *App) getCurrentWeekScoresAllUsers() (map[int]float64, error) {
@@ -2985,6 +3073,13 @@ func (a *App) getWeeklyStatsData() (*WeeklyStatsResponse, error) {
return nil, fmt.Errorf("error getting current week scores: %w", err) return nil, fmt.Errorf("error getting current week scores: %w", err)
} }
// Получаем сегодняшние приросты для всех пользователей
todayScores, err := a.getTodayScoresAllUsers()
if err != nil {
log.Printf("Error getting today scores: %v", err)
return nil, fmt.Errorf("error getting today scores: %w", err)
}
query := ` query := `
SELECT SELECT
p.id AS project_id, p.id AS project_id,
@@ -3047,6 +3142,11 @@ func (a *App) getWeeklyStatsData() (*WeeklyStatsResponse, error) {
project.TotalScore = currentWeekScore project.TotalScore = currentWeekScore
} }
// Добавляем сегодняшний прирост
if todayScore, exists := todayScores[projectID]; exists && todayScore != 0 {
project.TodayChange = &todayScore
}
if minGoalScore.Valid { if minGoalScore.Valid {
project.MinGoalScore = minGoalScore.Float64 project.MinGoalScore = minGoalScore.Float64
} else { } else {
@@ -3135,6 +3235,13 @@ func (a *App) getWeeklyStatsDataForUser(userID int) (*WeeklyStatsResponse, error
return nil, fmt.Errorf("error getting current week scores: %w", err) return nil, fmt.Errorf("error getting current week scores: %w", err)
} }
// Получаем сегодняшние приросты
todayScores, err := a.getTodayScores(userID)
if err != nil {
log.Printf("Error getting today scores: %v", err)
return nil, fmt.Errorf("error getting today scores: %w", err)
}
query := ` query := `
SELECT SELECT
p.id AS project_id, p.id AS project_id,
@@ -3193,6 +3300,11 @@ func (a *App) getWeeklyStatsDataForUser(userID int) (*WeeklyStatsResponse, error
project.TotalScore = currentWeekScore project.TotalScore = currentWeekScore
} }
// Добавляем сегодняшний прирост
if todayScore, exists := todayScores[projectID]; exists && todayScore != 0 {
project.TodayChange = &todayScore
}
if minGoalScore.Valid { if minGoalScore.Valid {
project.MinGoalScore = minGoalScore.Float64 project.MinGoalScore = minGoalScore.Float64
} else { } else {

View File

@@ -1,6 +1,6 @@
{ {
"name": "play-life-web", "name": "play-life-web",
"version": "4.13.6", "version": "4.14.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -92,7 +92,7 @@ function CircularProgressBar({ progress, size = 120, strokeWidth = 8, showCheckm
// Компонент карточки проекта с круглым прогрессбаром // Компонент карточки проекта с круглым прогрессбаром
function ProjectCard({ project, projectColor, onProjectClick }) { function ProjectCard({ project, projectColor, onProjectClick }) {
const { project_name, total_score, min_goal_score, max_goal_score, priority } = project const { project_name, total_score, min_goal_score, max_goal_score, priority, today_change } = project
// Вычисляем прогресс по оригинальной логике из ProjectProgressBar // Вычисляем прогресс по оригинальной логике из ProjectProgressBar
const getGoalProgress = () => { const getGoalProgress = () => {
@@ -155,6 +155,17 @@ function ProjectCard({ project, projectColor, onProjectClick }) {
return '0+' return '0+'
} }
// Форматируем сегодняшний прирост
const formatTodayChange = (value) => {
if (value === null || value === undefined) return '0'
const rounded = Math.round(value * 10) / 10
if (rounded === 0) return '0'
if (Number.isInteger(rounded)) {
return rounded > 0 ? `+${rounded}` : `${rounded}`
}
return rounded > 0 ? `+${rounded.toFixed(1)}` : `${rounded.toFixed(1)}`
}
const handleClick = () => { const handleClick = () => {
if (onProjectClick) { if (onProjectClick) {
onProjectClick(project_name) onProjectClick(project_name)
@@ -173,8 +184,15 @@ function ProjectCard({ project, projectColor, onProjectClick }) {
<div className="text-base font-semibold text-gray-600 leading-normal truncate mb-0.5"> <div className="text-base font-semibold text-gray-600 leading-normal truncate mb-0.5">
{project_name} {project_name}
</div> </div>
<div className="text-3xl font-bold text-black leading-normal mb-0.5"> <div className="flex items-center gap-2 mb-0.5">
{total_score?.toFixed(1) || '0.0'} <div className="text-3xl font-bold text-black leading-normal">
{total_score?.toFixed(1) || '0.0'}
</div>
{today_change !== null && today_change !== undefined && today_change !== 0 && (
<div className="text-base font-medium text-gray-400 leading-normal">
({formatTodayChange(today_change)})
</div>
)}
</div> </div>
<div className="text-xs text-gray-500 leading-normal"> <div className="text-xs text-gray-500 leading-normal">
Целевая зона: {getTargetZone()} Целевая зона: {getTargetZone()}