diff --git a/api/dashboard_charts.php b/api/dashboard_charts.php index 6a09946..a5e8b71 100644 --- a/api/dashboard_charts.php +++ b/api/dashboard_charts.php @@ -37,11 +37,11 @@ /* tek eksende gosterilebilir). */ /* ------------------------------------------------------------------ */ -$months = []; -for ($i = 11; $i >= 0; $i--) { - $months[date('Y-m', strtotime("-{$i} month"))] = ['opened' => 0, 'closed' => 0]; -} -$since = date('Y-m-01', strtotime('-11 month')); +// Takvim ayi penceresi ayin 1'ine sabitlenerek kurulur (bkz. recent_months): +// strtotime('-N month') ay sonu gunlerinde bir sonraki aya tasiyordu. +$monthKeys = recent_months(12); +$months = array_fill_keys($monthKeys, ['opened' => 0, 'closed' => 0]); +$since = $monthKeys[0] . '-01'; $rows = $pdo->prepare( "SELECT DATE_FORMAT(created_at, '%Y-%m') AS ay, COUNT(*) AS adet diff --git a/includes/functions.php b/includes/functions.php index 92f9613..b0a854b 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -216,6 +216,36 @@ function days_until(?string $date): ?int return (int)floor(($t - strtotime(date('Y-m-d'))) / 86400); } +/** + * Son $count takvim ayinin 'Y-m' anahtarlarini eskiden yeniye sirali dondurur. + * Pencerenin alt siniri (SQL :since degeri) ilk anahtarin '-01' ekidir. + * + * NEDEN AYRI BIR FONKSIYON: strtotime('-N month') gun tasmasini kirmaz; + * ayin 29-31'inde hedef ayda o gun yoksa sonuc bir SONRAKI aya taser. + * Dashboard trend penceresi (api/dashboard_charts.php) bu yuzden ay sonu + * isteklerinde 12 yerine 7-11 ay uretiyor, kayip aylarin riskleri hic + * sayilmiyordu. Burada ay aritmetigi her zaman ayin 1'ine sabitlenir; + * 1. gunden cikarilan ay asla tasmaz. + * + * @param int $count Kac ay dondurecek (1 = yalnizca bulunulan ay). + * @param int|null $baseTs Pencerenin bittigi an (null = simdi). + * @return list 'YYYY-MM' anahtarlari, eskiden yeniye. + */ +function recent_months(int $count, ?int $baseTs = null): array +{ + if ($count < 1) { + return []; + } + $anchor = (new DateTimeImmutable(date('Y-m-d H:i:s', $baseTs ?? time()))) + ->modify('first day of this month midnight'); + + $out = []; + for ($i = $count - 1; $i >= 0; $i--) { + $out[] = $anchor->modify("-{$i} months")->format('Y-m'); + } + return $out; +} + function str_limit(?string $text, int $limit = 80): string { $text = trim((string)$text); diff --git a/reports/executive_summary.php b/reports/executive_summary.php index f23ee2d..b18584e 100644 --- a/reports/executive_summary.php +++ b/reports/executive_summary.php @@ -85,12 +85,12 @@ LIMIT 8" )->fetchAll(); -/* Son 6 ay: açılan ve kapanan */ -$trend = []; -for ($i = 5; $i >= 0; $i--) { - $trend[date('Y-m', strtotime("-{$i} month"))] = ['acilan' => 0, 'kapanan' => 0]; -} -$since = date('Y-m-01', strtotime('-5 month')); +/* Son 6 ay: açılan ve kapanan — takvim ayı penceresi ayın 1'ine + sabitlenerek kurulur (bkz. recent_months): strtotime('-N month') + ay sonu günlerinde bir sonraki aya taşıyordu. */ +$monthKeys = recent_months(6); +$trend = array_fill_keys($monthKeys, ['acilan' => 0, 'kapanan' => 0]); +$since = $monthKeys[0] . '-01'; $q = db()->prepare("SELECT DATE_FORMAT(created_at,'%Y-%m') AS ay, COUNT(*) AS n FROM risks WHERE deleted_at IS NULL AND created_at >= :s GROUP BY ay"); diff --git a/tools/trend_months_test.php b/tools/trend_months_test.php new file mode 100644 index 0000000..5ea9d5a --- /dev/null +++ b/tools/trend_months_test.php @@ -0,0 +1,147 @@ += 0; $k--) { + $out[] = date('Y-m', mktime(1, 1, 1, $m - $k, 1, $y)); + } + return $out; +} + +echo "\n=============== Trend Month Window Regression Test ==============="; + +/* ------------------------------------------------------------------ */ +section('1. Ay sonu gunlerinde pencere (regresyon)'); +/* ------------------------------------------------------------------ */ + +// Hatali davranisin kanitlandigi gunler: 29-31 (hedef ay kisa ise tasma). +$probes = [ + '2026-05-31', '2026-03-31', '2026-08-31', '2025-12-31', + '2026-01-31', '2026-01-30', '2024-02-29', '2026-02-28', + '2026-10-31', '2026-04-30', '2025-11-30', '2026-12-31', +]; + +foreach ($probes as $d) { + $bts = strtotime($d . ' 12:00:00'); + $got = recent_months(12, $bts); + check( + "12 ay: {$d}", + $got === expected_months($bts, 12) && count(array_unique($got)) === 12, + count($got) . ' ay' + ); +} + +/* ------------------------------------------------------------------ */ +section('2. Sinir ve ikincil dallar'); +/* ------------------------------------------------------------------ */ + +$now = time(); +check('bugun (varsayilan taban): 12 ay', + recent_months(12) === expected_months($now, 12)); +check('taban null acik gecilirse ayni sonuc', + recent_months(12, null) === recent_months(12)); +check('count=1 yalnizca bulunulan ay', + recent_months(1, $now) === [date('Y-m', $now)]); +check('count=0 bos liste', recent_months(0, $now) === []); +check('count negatif bos liste', recent_months(-3, $now) === []); +check('count=13 yil sinirini asar', + recent_months(13, strtotime('2026-01-15 12:00:00')) + === expected_months(strtotime('2026-01-15 12:00:00'), 13)); +check('count=6 yonetici ozeti penceresi (2026-05-31 taban)', + recent_months(6, strtotime('2026-05-31 12:00:00')) + === expected_months(strtotime('2026-05-31 12:00:00'), 6)); +check('anahtarlar eskiden yeniye sirali', + recent_months(12, $now) === array_values(array_sort(recent_months(12, $now)))); + +/* SQL :since degeri: en eski ayin 1'i (api/dashboard_charts.php tuketimi) */ +$keys = recent_months(12, strtotime('2026-05-31 12:00:00')); +check('since = en eski ayin 1\'i (2026-05-31 taban)', + $keys[0] . '-01' === '2025-06-01', $keys[0] . '-01'); + +/* ------------------------------------------------------------------ */ +section('3. Uc nokta baglantisi'); +/* ------------------------------------------------------------------ */ + +$src = (string)file_get_contents(__DIR__ . '/../api/dashboard_charts.php'); +check('api/dashboard_charts.php recent_months() kullaniyor', + str_contains($src, 'recent_months(12)')); +check('dashboard bozuk strtotime("-N month") ifadesi kaldirildi', + !str_contains($src, 'strtotime("-{')); + +$srcExec = (string)file_get_contents(__DIR__ . '/../reports/executive_summary.php'); +check('reports/executive_summary.php recent_months() kullaniyor', + str_contains($srcExec, 'recent_months(6)')); +check('yonetici ozeti bozuk strtotime ay ifadeleri kaldirildi', + !str_contains($srcExec, 'strtotime("-{') && !str_contains($srcExec, "strtotime('-5 month')")); + +/* ------------------------------------------------------------------ */ + +echo "\n" . str_repeat('-', 72) . "\n"; +printf("Sonuc: %d OK, %d FAIL\n", $PASS, $FAIL); +if ($FAIL > 0) { + echo "TREND MONTH WINDOW TEST: FAIL\n"; + exit(1); +} +echo "TREND MONTH WINDOW TEST: PASS\n"; +exit(0); + +/** Kucukten buyuge siralar (strcmp) — 'Y-m' anahtarlari icin yeterli. */ +function array_sort(array $a): array +{ + usort($a, static fn(string $x, string $y) => strcmp($x, $y)); + return $a; +}