fix(defter): haftalar ayın günlerine göre (1–8, 9–15, 16–22, 23–son), defter bugünün haftasıyla açılır

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-13 20:31:15 +03:00
parent 233907e3d9
commit 6e1000afd2
15 changed files with 144 additions and 130 deletions
+10 -18
View File
@@ -1,5 +1,5 @@
import { useRef, useState } from 'react'
import { useLocation, useSearchParams } from 'react-router'
import { useLocation } from 'react-router'
import { Info } from 'lucide-react'
import { PageError, PageHeader, PageLoading } from '@/components/page'
import { PeriodPicker } from '@/features/periods/period-picker'
@@ -8,7 +8,7 @@ import { Section } from '@/features/planning/plan-read'
import { useCategories, usePaymentMethods } from '@/lib/catalog'
import { useEntries } from '@/lib/entries'
import { toIsoDate } from '@/lib/format'
import { parseWeekParam, weekIndexFor } from '@/lib/ledger'
import { weekIndexFor } from '@/lib/ledger'
import { isWritable } from '@/lib/period-select'
import { useBudget } from '@/lib/periods'
import { t } from '@/lib/strings'
@@ -33,11 +33,11 @@ export function LedgerPage() {
}
/**
* Hafta adreste tutulur (`?hafta=2`): yenileyince / geri gelince aynı hafta açılır.
* Defter her açılışta **bugünün haftasıyla** başlar (dönem geçmişteyse son, gelecekteyse ilk
* hafta); oklarla gezilen hafta yalnızca bu ekranda tutulur, adrese yazılmaz (kullanıcı kararı).
* Düzen: hafta seçici → form → haftalık özet → girişler (masaüstünde form solda, diğerleri sağda).
*/
function LedgerView({ period }: { period: Period }) {
const [params, setParams] = useSearchParams()
const [today] = useState(() => toIsoDate(new Date()))
const [editing, setEditing] = useState<Entry | null>(null)
// Panel'deki "Harcama Ekle" → tutar alanına odaklan
@@ -45,9 +45,9 @@ function LedgerView({ period }: { period: Period }) {
const formRef = useRef<HTMLDivElement>(null)
const inPeriod = today >= period.start_date && today <= period.last_date
const todayIndex = inPeriod ? weekIndexFor(period.weeks, today) : null
const index =
parseWeekParam(params.get('hafta'), period.weeks.length) ?? weekIndexFor(period.weeks, today)
const week = period.weeks[index - 1]
const [index, setIndex] = useState(() => weekIndexFor(period.weeks, today))
// Dönem tarihleri değişip hafta sayısı azalırsa son haftaya düş
const week = period.weeks[Math.min(index, period.weeks.length) - 1]
const readOnly = !isWritable(period)
const budget = useBudget(period.id)
@@ -57,15 +57,7 @@ function LedgerView({ period }: { period: Period }) {
const goTo = (n: number) => {
setEditing(null)
setParams(
(prev) => {
const next = new URLSearchParams(prev) // seçili dönem (`donem`) korunur
if (n === todayIndex) next.delete('hafta')
else next.set('hafta', String(n))
return next
},
{ replace: true },
)
setIndex(n)
}
function edit(e: Entry) {
@@ -81,7 +73,7 @@ function LedgerView({ period }: { period: Period }) {
<div className="mx-auto max-w-3xl lg:max-w-none">
<WeekPicker
weeks={period.weeks}
index={index}
index={week.index}
todayIndex={todayIndex}
budget={budget.data}
onChange={goTo}
@@ -123,7 +115,7 @@ function LedgerView({ period }: { period: Period }) {
{budget.isError ? (
<PageError message={budget.error.message} onRetry={budget.refetch} />
) : budget.data ? (
<WeekSummary budget={budget.data} index={index} />
<WeekSummary budget={budget.data} index={week.index} />
) : (
<PageLoading />
)}
+1 -1
View File
@@ -7,7 +7,7 @@ import type { PeriodWeek } from '@/types/PeriodWeek'
const s = t.ledger
/** ◀ önceki | 2. hafta / 5 · 07.12 – 13.12 | sonraki ▶ (+ "Bu hafta", haftanın toplamı) */
/** ◀ önceki | 2. hafta / 4 · 09.12 – 15.12 | sonraki ▶ (+ "Bu hafta", haftanın toplamı) */
export function WeekPicker({
weeks,
index,
+5 -4
View File
@@ -28,14 +28,15 @@ describe('splitProportional (backend ile aynı)', () => {
})
describe('previewWeeks', () => {
const dec = [6, 7, 7, 7, 4]
// Aralık: 1–8, 9–15, 16–22, 23–31 (backend tests/budget.rs ile aynı)
const dec = [8, 7, 7, 9]
it('all automatic', () => {
const r = previewWeeks(
100_000,
dec.map((days) => ({ days, manual: null })),
)
expect(r.weeks.map((w) => w.amount)).toEqual([19_355, 22_581, 22_581, 22_580, 12_903])
expect(r.weeks.map((w) => w.amount)).toEqual([25_806, 22_581, 22_581, 29_032])
expect(r.weeklyOver).toBe(false)
})
@@ -45,9 +46,9 @@ describe('previewWeeks', () => {
dec.map((days, i) => ({ days, manual: i === 1 ? 40_000 : null })),
)
expect(r.weeks[1]).toEqual({ amount: 40_000, isManual: true })
// kalan 60.000 → 6,7,7,4 gün
// kalan 60.000 → 8,7,9 gün
expect(r.weeks.filter((w) => !w.isManual).map((w) => w.amount)).toEqual([
15_000, 17_500, 17_500, 10_000,
20_000, 17_500, 22_500,
])
expect(r.weeks.reduce((a, w) => a + w.amount, 0)).toBe(100_000)
})
+14 -22
View File
@@ -1,38 +1,30 @@
import { describe, expect, it } from 'vitest'
import type { PeriodWeek } from '@/types/PeriodWeek'
import type { Entry } from '@/types/Entry'
import { defaultEntryDate, groupByDay, parseWeekParam, weekIndexFor } from './ledger'
import { defaultEntryDate, groupByDay, weekIndexFor } from './ledger'
// Aralık 2026: 1–6 · 7–13 · 14–20 · 21–27 · 28–31 (backend testleriyle aynı)
// Aralık 2026: 1–8 · 9–15 · 16–22 · 23–31 (backend `calendar::weeks` ile aynı)
const weeks: PeriodWeek[] = [
['2026-12-01', '2026-12-06'],
['2026-12-07', '2026-12-13'],
['2026-12-14', '2026-12-20'],
['2026-12-21', '2026-12-27'],
['2026-12-28', '2026-12-31'],
['2026-12-01', '2026-12-08'],
['2026-12-09', '2026-12-15'],
['2026-12-16', '2026-12-22'],
['2026-12-23', '2026-12-31'],
].map(([start_date, last_date], i) => ({ index: i + 1, start_date, last_date, days: 0 }))
describe('ledger', () => {
it('finds the week of a date, clamping outside the period', () => {
expect(weekIndexFor(weeks, '2026-12-06')).toBe(1)
expect(weekIndexFor(weeks, '2026-12-07')).toBe(2)
expect(weekIndexFor(weeks, '2026-12-31')).toBe(5)
expect(weekIndexFor(weeks, '2026-12-08')).toBe(1)
expect(weekIndexFor(weeks, '2026-12-09')).toBe(2)
expect(weekIndexFor(weeks, '2026-12-23')).toBe(4)
expect(weekIndexFor(weeks, '2026-12-31')).toBe(4)
expect(weekIndexFor(weeks, '2026-11-12')).toBe(1)
expect(weekIndexFor(weeks, '2027-01-03')).toBe(5)
expect(weekIndexFor(weeks, '2027-01-03')).toBe(4)
})
it('defaults the entry date to today, or the nearest day of the week', () => {
expect(defaultEntryDate(weeks[1], '2026-12-09')).toBe('2026-12-09')
expect(defaultEntryDate(weeks[1], '2026-12-20')).toBe('2026-12-13') // geçmiş hafta → son gün
expect(defaultEntryDate(weeks[1], '2026-12-02')).toBe('2026-12-07') // gelecek hafta → ilk gün
})
it('accepts only valid week numbers from the address', () => {
expect(parseWeekParam('3', 5)).toBe(3)
expect(parseWeekParam('0', 5)).toBeNull()
expect(parseWeekParam('6', 5)).toBeNull()
expect(parseWeekParam('2a', 5)).toBeNull()
expect(parseWeekParam(null, 5)).toBeNull()
expect(defaultEntryDate(weeks[1], '2026-12-13')).toBe('2026-12-13')
expect(defaultEntryDate(weeks[1], '2026-12-20')).toBe('2026-12-15') // geçmiş hafta → son gün
expect(defaultEntryDate(weeks[1], '2026-12-02')).toBe('2026-12-09') // gelecek hafta → ilk gün
})
it('groups entries by day, newest day first', () => {
-7
View File
@@ -21,13 +21,6 @@ export function defaultEntryDate(week: PeriodWeek, today: string): string {
return today
}
/** Adresteki `?hafta=` değeri → geçerli hafta numarası ya da `null` */
export function parseWeekParam(value: string | null, count: number): number | null {
if (!value || !/^\d+$/.test(value)) return null
const n = Number(value)
return n >= 1 && n <= count ? n : null
}
export type EntryDay = { date: string; total: number; entries: Entry[] }
/** Kayıtları gün gün toplar; en yeni gün başta, gün içinde sunucu sırası korunur */
+1 -1
View File
@@ -126,7 +126,7 @@ export const t = {
last: 'Son gün',
name: 'Dönem adı',
nameHint: 'Boş bırakırsan tarihlere göre otomatik verilir (ör. "Aralık 2026").',
days: (n: number) => `${n} gün · haftalar Pazartesi başlar`,
days: (n: number) => `${n} gün · haftalar dönemin 1., 9., 16. ve 23. gününde başlar`,
created: 'Dönem açıldı',
startRequired: 'Başlangıç tarihini seçin',
lastRequired: 'Son günü seçin',