@@ -1431,20 +1431,20 @@ func (a *App) getWeeklyStatsHandler(w http.ResponseWriter, r *http.Request) {
COALESCE(wr.total_score, 0.0000) AS total_score,
wg.min_goal_score,
wg.max_goal_score,
wg. priority
COALESCE(wg.priority, p.priority) AS priority
FROM
weekly_goals wg
JOIN
projects p ON wg.project_id = p.id
projects p
LEFT JOIN
weekly_goals wg ON wg.project_id = p.id
AND wg.goal_year = EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER
AND wg.goal_week = EXTRACT(WEEK FROM CURRENT_DATE)::INTEGER
LEFT JOIN
weekly_report_mv wr
ON wg.project_ id = wr.project_id
AND wg.goal_year = wr.report_year
AND wg.goal_week = wr.report_week
ON p. id = wr.project_id
AND EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER = wr.report_year
AND EXTRACT(WEEK FROM CURRENT_DATE)::INTEGER = wr.report_week
WHERE
-- Фильтруем ТОЛЬКО по целям текущего года и недели
wg.goal_year = EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER
AND wg.goal_week = EXTRACT(WEEK FROM CURRENT_DATE)::INTEGER
p.deleted = FALSE
ORDER BY
total_score DESC
`
@@ -1463,13 +1463,14 @@ func (a *App) getWeeklyStatsHandler(w http.ResponseWriter, r *http.Request) {
for rows . Next ( ) {
var project WeeklyProjectStats
var minGoalScore sql . NullFloat64
var maxGoalScore sql . NullFloat64
var priority sql . NullInt64
err := rows . Scan (
& project . ProjectName ,
& project . TotalScore ,
& project . M inGoalScore,
& m inGoalScore,
& maxGoalScore ,
& priority ,
)
@@ -1479,6 +1480,12 @@ func (a *App) getWeeklyStatsHandler(w http.ResponseWriter, r *http.Request) {
return
}
if minGoalScore . Valid {
project . MinGoalScore = minGoalScore . Float64
} else {
project . MinGoalScore = 0
}
if maxGoalScore . Valid {
maxGoalVal := maxGoalScore . Float64
project . MaxGoalScore = & maxGoalVal
@@ -1492,7 +1499,7 @@ func (a *App) getWeeklyStatsHandler(w http.ResponseWriter, r *http.Request) {
// Расчет calculated_score по формуле из n8n
totalScore := project . TotalScore
minGoalScore := project . MinGoalScore
minGoalScoreVal := project . MinGoalScore
var maxGoalScoreVal float64
if project . MaxGoalScore != nil {
maxGoalScoreVal = * project . MaxGoalScore
@@ -1508,15 +1515,15 @@ func (a *App) getWeeklyStatsHandler(w http.ResponseWriter, r *http.Request) {
// Расчет базового прогресса
var baseProgress float64
if minGoalScore > 0 {
baseProgress = ( min ( totalScore , minGoalScore ) / minGoalScore ) * 100.0
if minGoalScoreVal > 0 {
baseProgress = ( min ( totalScore , minGoalScoreVal ) / minGoalScoreVal ) * 100.0
}
// Расчет экстра прогресса
var extraProgress float64
denominator := maxGoalScoreVal - minGoalScore
if denominator > 0 && totalScore > minGoalScore {
excess := min ( totalScore , maxGoalScoreVal ) - minGoalScore
denominator := maxGoalScoreVal - minGoalScoreVal
if denominator > 0 && totalScore > minGoalScoreVal {
excess := min ( totalScore , maxGoalScoreVal ) - minGoalScoreVal
extraProgress = ( excess / denominator ) * extraBonusLimit
}
@@ -1524,23 +1531,45 @@ func (a *App) getWeeklyStatsHandler(w http.ResponseWriter, r *http.Request) {
project . CalculatedScore = roundToTwoDecimals ( resultScore )
// Группировка для итогового расчета
if _ , exists := groups [ priorityVal ] ; ! exists {
groups [ priority Val ] = make ( [ ] float64 , 0 )
// Проекты с minGoal = 0 или null не учитываются в общем проценте выполнения
if minGoalScore Val > 0 {
if _ , exists := groups [ priorityVal ] ; ! exists {
groups [ priorityVal ] = make ( [ ] float64 , 0 )
}
groups [ priorityVal ] = append ( groups [ priorityVal ] , project . CalculatedScore )
}
groups [ priorityVal ] = append ( groups [ priorityVal ] , project . CalculatedScore )
projects = append ( projects , project )
}
// Находим среднее внутри каждой группы
groupAverages := make ( [ ] float64 , 0 )
for _ , scores := range groups {
for priorityVal , scores := range groups {
if len ( scores ) > 0 {
sum := 0.0
for _ , score := range scores {
sum += score
var avg float64
// Для приоритета 1 и 2 - обычное среднее (как было)
if priorityVal == 1 || priorityVal == 2 {
sum := 0.0
for _ , score := range scores {
sum += score
}
avg = sum / float64 ( len ( scores ) )
} else {
// Для проектов без приоритета (priorityVal == 0) - новая формула
projectCount := float64 ( len ( scores ) )
multiplier := 100.0 / math . Floor ( projectCount * 0.8 )
sum := 0.0
for _ , score := range scores {
// score уже в процентах (например, 80.0), переводим в долю (0.8)
scoreAsDecimal := score / 100.0
sum += scoreAsDecimal * multiplier
}
avg = math . Min ( 120.0 , sum )
}
avg := sum / float64 ( len ( scores ) )
groupAverages = append ( groupAverages , avg )
}
}
@@ -1926,23 +1955,32 @@ func (a *App) initPlayLifeDB() error {
func ( a * App ) startWeeklyGoalsScheduler ( ) {
// Получаем часовой пояс из переменной окружения (по умолчанию UTC)
timezoneStr := getEnv ( "TIMEZONE" , "UTC" )
log . Printf ( "Loading timezone for weekly goals scheduler: '%s'" , timezoneStr )
// Загружаем часовой пояс
loc , err := time . LoadLocation ( timezoneStr )
if err != nil {
log . Printf ( "Warning: Invalid timezone '%s': %v. Using UTC instead." , timezoneStr , err )
log . Printf ( "Note: Timezone must be in IANA format (e.g., 'Europe/Moscow', 'America/New_York'), not 'UTC+3'" )
loc = time . UTC
timezoneStr = "UTC"
} else {
log . Printf ( "S cheduler timezone set to: %s" , timezoneStr )
log . Printf ( "Weekly goals s cheduler timezone set to: %s" , timezoneStr )
}
// Логируем текущее время в указанном часовом поясе для проверки
now := time . Now ( ) . In ( loc )
log . Printf ( "Current time in scheduler timezone (%s): %s" , timezoneStr , now . Format ( "2006-01-02 15:04:05 MST" ) )
log . Printf ( "Next weekly goals setup will be on Monday at: 06:00 %s (cron: '0 6 * * 1')" , timezoneStr )
// Создаем планировщик с указанным часовым поясом
c := cron . New ( cron . WithLocation ( loc ) )
// Добавляем задачу: каждый понедельник в 6:00 утра
// Cron выражение: "0 6 * * 1" означает: минута=0, час=6, любой день месяца, любой месяц, понедельник (1)
_ , err = c . AddFunc ( "0 6 * * 1" , func ( ) {
log . Printf ( "Scheduled task: Setting up weekly goals (timezone: %s)" , timezoneStr )
now := time . Now ( ) . In ( loc )
log . Printf ( "Scheduled task: Setting up weekly goals (timezone: %s, local time: %s)" , timezoneStr , now . Format ( "2006-01-02 15:04:05 MST" ) )
if err := a . setupWeeklyGoals ( ) ; err != nil {
log . Printf ( "Error in scheduled weekly goals setup: %v" , err )
}
@@ -1955,7 +1993,7 @@ func (a *App) startWeeklyGoalsScheduler() {
// Запускаем планировщик
c . Start ( )
log . Println ( "Weekly goals scheduler started: every Monday at 6:00 AM" )
log . Printf ( "Weekly goals scheduler started: every Monday at 6:00 AM %s" , timezoneStr )
// Планировщик будет работать в фоновом режиме
}
@@ -1976,21 +2014,20 @@ func (a *App) getWeeklyStatsData() (*WeeklyStatsResponse, error) {
COALESCE(wr.total_score, 0.0000) AS total_score,
wg.min_goal_score,
wg.max_goal_score,
wg. priority
COALESCE(wg.priority, p.priority) AS priority
FROM
weekly_goals wg
JOIN
projects p ON wg.project_id = p.id
projects p
LEFT JOIN
weekly_goals wg ON wg.project_id = p.id
AND wg.goal_year = EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER
AND wg.goal_week = EXTRACT(WEEK FROM CURRENT_DATE)::INTEGER
LEFT JOIN
weekly_report_mv wr
ON wg.project_ id = wr.project_id
AND wg.goal_year = wr.report_year
AND wg.goal_week = wr.report_week
ON p. id = wr.project_id
AND EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER = wr.report_year
AND EXTRACT(WEEK FROM CURRENT_DATE)::INTEGER = wr.report_week
WHERE
-- Фильтруем ТОЛЬКО по целям текущего года и недели
wg.goal_year = EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER
AND wg.goal_week = EXTRACT(WEEK FROM CURRENT_DATE)::INTEGER
AND p.deleted = FALSE
p.deleted = FALSE
ORDER BY
total_score DESC
`
@@ -2008,13 +2045,14 @@ func (a *App) getWeeklyStatsData() (*WeeklyStatsResponse, error) {
for rows . Next ( ) {
var project WeeklyProjectStats
var minGoalScore sql . NullFloat64
var maxGoalScore sql . NullFloat64
var priority sql . NullInt64
err := rows . Scan (
& project . ProjectName ,
& project . TotalScore ,
& project . M inGoalScore,
& m inGoalScore,
& maxGoalScore ,
& priority ,
)
@@ -2023,6 +2061,12 @@ func (a *App) getWeeklyStatsData() (*WeeklyStatsResponse, error) {
return nil , fmt . Errorf ( "error scanning weekly stats row: %w" , err )
}
if minGoalScore . Valid {
project . MinGoalScore = minGoalScore . Float64
} else {
project . MinGoalScore = 0
}
if maxGoalScore . Valid {
maxGoalVal := maxGoalScore . Float64
project . MaxGoalScore = & maxGoalVal
@@ -2036,7 +2080,7 @@ func (a *App) getWeeklyStatsData() (*WeeklyStatsResponse, error) {
// Расчет calculated_score по формуле из n8n
totalScore := project . TotalScore
minGoalScore := project . MinGoalScore
minGoalScoreVal := project . MinGoalScore
var maxGoalScoreVal float64
if project . MaxGoalScore != nil {
maxGoalScoreVal = * project . MaxGoalScore
@@ -2052,15 +2096,15 @@ func (a *App) getWeeklyStatsData() (*WeeklyStatsResponse, error) {
// Расчет базового прогресса
var baseProgress float64
if minGoalScore > 0 {
baseProgress = ( min ( totalScore , minGoalScore ) / minGoalScore ) * 100.0
if minGoalScoreVal > 0 {
baseProgress = ( min ( totalScore , minGoalScoreVal ) / minGoalScoreVal ) * 100.0
}
// Расчет экстра прогресса
var extraProgress float64
denominator := maxGoalScoreVal - minGoalScore
if denominator > 0 && totalScore > minGoalScore {
excess := min ( totalScore , maxGoalScoreVal ) - minGoalScore
denominator := maxGoalScoreVal - minGoalScoreVal
if denominator > 0 && totalScore > minGoalScoreVal {
excess := min ( totalScore , maxGoalScoreVal ) - minGoalScoreVal
extraProgress = ( excess / denominator ) * extraBonusLimit
}
@@ -2068,23 +2112,45 @@ func (a *App) getWeeklyStatsData() (*WeeklyStatsResponse, error) {
project . CalculatedScore = roundToTwoDecimals ( resultScore )
// Группировка для итогового расчета
if _ , exists := groups [ priorityVal ] ; ! exists {
groups [ priority Val ] = make ( [ ] float64 , 0 )
// Проекты с minGoal = 0 или null не учитываются в общем проценте выполнения
if minGoalScore Val > 0 {
if _ , exists := groups [ priorityVal ] ; ! exists {
groups [ priorityVal ] = make ( [ ] float64 , 0 )
}
groups [ priorityVal ] = append ( groups [ priorityVal ] , project . CalculatedScore )
}
groups [ priorityVal ] = append ( groups [ priorityVal ] , project . CalculatedScore )
projects = append ( projects , project )
}
// Находим среднее внутри каждой группы
groupAverages := make ( [ ] float64 , 0 )
for _ , scores := range groups {
for priorityVal , scores := range groups {
if len ( scores ) > 0 {
sum := 0.0
for _ , score := range scores {
sum += score
var avg float64
// Для приоритета 1 и 2 - обычное среднее (как было)
if priorityVal == 1 || priorityVal == 2 {
sum := 0.0
for _ , score := range scores {
sum += score
}
avg = sum / float64 ( len ( scores ) )
} else {
// Для проектов без приоритета (priorityVal == 0) - новая формула
projectCount := float64 ( len ( scores ) )
multiplier := 100.0 / math . Floor ( projectCount * 0.8 )
sum := 0.0
for _ , score := range scores {
// score уже в процентах (например, 80.0), переводим в долю (0.8)
scoreAsDecimal := score / 100.0
sum += scoreAsDecimal * multiplier
}
avg = math . Min ( 120.0 , sum )
}
avg := sum / float64 ( len ( scores ) )
groupAverages = append ( groupAverages , avg )
}
}
@@ -2191,27 +2257,36 @@ func (a *App) sendDailyReport() error {
}
// startDailyReportScheduler запускает планировщик для ежедневного отчета
// каждый день в 11 :59 в указанном часовом поясе
// каждый день в 23 :59 в указанном часовом поясе
func ( a * App ) startDailyReportScheduler ( ) {
// Получаем часовой пояс из переменной окружения (по умолчанию UTC)
timezoneStr := getEnv ( "TIMEZONE" , "UTC" )
log . Printf ( "Loading timezone for daily report scheduler: '%s'" , timezoneStr )
// Загружаем часовой пояс
loc , err := time . LoadLocation ( timezoneStr )
if err != nil {
log . Printf ( "Warning: Invalid timezone '%s': %v. Using UTC instead." , timezoneStr , err )
log . Printf ( "Note: Timezone must be in IANA format (e.g., 'Europe/Moscow', 'America/New_York'), not 'UTC+3'" )
loc = time . UTC
timezoneStr = "UTC"
} else {
log . Printf ( "Daily report scheduler timezone set to: %s" , timezoneStr )
}
// Логируем текущее время в указанном часовом поясе для проверки
now := time . Now ( ) . In ( loc )
log . Printf ( "Current time in scheduler timezone (%s): %s" , timezoneStr , now . Format ( "2006-01-02 15:04:05 MST" ) )
log . Printf ( "Next daily report will be sent at: 23:59 %s (cron: '59 23 * * *')" , timezoneStr )
// Создаем планировщик с указанным часовым поясом
c := cron . New ( cron . WithLocation ( loc ) )
// Добавляем задачу: каждый день в 11 :59
// Cron выражение: "59 11 * * *" означает: минута=59, час=11 , любой день месяца, любой месяц, любой день недели
_ , err = c . AddFunc ( "59 11 * * *" , func ( ) {
log . Printf ( "Scheduled task: Sending daily report (timezone: %s)" , timezoneStr )
// Добавляем задачу: каждый день в 23 :59
// Cron выражение: "59 23 * * *" означает: минута=59, час=23 , любой день месяца, любой месяц, любой день недели
_ , err = c . AddFunc ( "59 23 * * *" , func ( ) {
now := time . Now ( ) . In ( loc )
log . Printf ( "Scheduled task: Sending daily report (timezone: %s, local time: %s)" , timezoneStr , now . Format ( "2006-01-02 15:04:05 MST" ) )
if err := a . sendDailyReport ( ) ; err != nil {
log . Printf ( "Error in scheduled daily report: %v" , err )
}
@@ -2224,7 +2299,7 @@ func (a *App) startDailyReportScheduler() {
// Запускаем планировщик
c . Start ( )
log . Println ( "Daily report scheduler started: every day at 11 :59 AM" )
log . Printf ( "Daily report scheduler started: every day at 23 :59 %s" , timezoneStr )
// Планировщик будет работать в фоновом режиме
}
@@ -2331,7 +2406,7 @@ func main() {
// Запускаем планировщик для автоматической фиксации целей на неделю
app . startWeeklyGoalsScheduler ( )
// Запускаем планировщик для ежедневного отчета в 11 :59
// Запускаем планировщик для ежедневного отчета в 23 :59
app . startDailyReportScheduler ( )
r := mux . NewRouter ( )
@@ -2876,7 +2951,7 @@ func (a *App) setupWeeklyGoals() error {
EXTRACT(WEEK FROM CURRENT_DATE)::INTEGER AS c_week
),
goal_metrics AS (
-- Считаем медиану на основе последних 12 записей из вьюхи
-- Считаем медиану на основе данных за 3 месяца (12 недель), исключая текущую неделю
SELECT
project_id,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY total_score) AS median_score
@@ -2884,11 +2959,19 @@ func (a *App) setupWeeklyGoals() error {
SELECT
project_id,
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
-- Исключаем текущую неделю и все будущие недели
-- Используем сравнение (year, week) < (current_year, current_week) для корректного исключения
(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 <= 12 -- Берем историю за последние 12 недель активности
WHERE rn <= 12 -- Берем историю за последние 12 недель (3 месяца), исключая текущую неделю
GROUP BY project_id
)
INSERT INTO weekly_goals (
@@ -2903,13 +2986,15 @@ func (a *App) setupWeeklyGoals() error {
p.id,
ci.c_year,
ci.c_week,
-- Если нет данных (gm.median_score IS NULL), используем 0 (значение по умолчанию)
COALESCE(gm.median_score, 0) AS min_goal_score,
-- Логика max_score в зависимости от приоритета
-- Логика max_score в зависимости от приоритета (только если есть данные)
CASE
WHEN p.priority = 1 THEN COALESCE(gm.median_score, 0) * 1.5
WHEN p.priority = 2 THEN COALESCE( gm.median_score, 0) * 1.3
ELSE COALESCE( gm.median_score, 0) * 1.2
END + (CASE WHEN COALESCE(gm.median_score, 0) = 0 THEN 10 ELSE 0 END) AS max_goal_score,
WHEN gm.median_score IS NULL THEN NULL
WHEN p.priority = 1 THEN gm.median_score * 1.5
WHEN p.priority = 2 THEN gm.median_score * 1.3
ELSE gm.median_score * 1.2
END AS max_goal_score,
p.priority
FROM projects p
CROSS JOIN current_info ci