6e1000afd2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
146 lines
5.5 KiB
TypeScript
146 lines
5.5 KiB
TypeScript
import { useRef, useState } from 'react'
|
||
import { useLocation } from 'react-router'
|
||
import { Info } from 'lucide-react'
|
||
import { PageError, PageHeader, PageLoading } from '@/components/page'
|
||
import { PeriodPicker } from '@/features/periods/period-picker'
|
||
import { SelectedPeriod } from '@/features/periods/selected-period'
|
||
import { Section } from '@/features/planning/plan-read'
|
||
import { useCategories, usePaymentMethods } from '@/lib/catalog'
|
||
import { useEntries } from '@/lib/entries'
|
||
import { toIsoDate } from '@/lib/format'
|
||
import { weekIndexFor } from '@/lib/ledger'
|
||
import { isWritable } from '@/lib/period-select'
|
||
import { useBudget } from '@/lib/periods'
|
||
import { t } from '@/lib/strings'
|
||
import type { Entry } from '@/types/Entry'
|
||
import type { Period } from '@/types/Period'
|
||
import { EntryForm } from './entry-form'
|
||
import { EntryList } from './entry-list'
|
||
import { WeekPicker } from './week-picker'
|
||
import { WeekSummary } from './week-summary'
|
||
|
||
export function LedgerPage() {
|
||
return (
|
||
<>
|
||
<PageHeader
|
||
title={t.pages.ledger.title}
|
||
description={t.pages.ledger.description}
|
||
actions={<PeriodPicker />}
|
||
/>
|
||
<SelectedPeriod>{(p) => <LedgerView key={p.id} period={p} />}</SelectedPeriod>
|
||
</>
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 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 [today] = useState(() => toIsoDate(new Date()))
|
||
const [editing, setEditing] = useState<Entry | null>(null)
|
||
// Panel'deki "Harcama Ekle" → tutar alanına odaklan
|
||
const focusAmount = (useLocation().state as { focus?: boolean } | null)?.focus === true
|
||
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, 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)
|
||
const categories = useCategories(true)
|
||
const methods = usePaymentMethods(true)
|
||
const entries = useEntries(period.id, { from: week.start_date, to: week.last_date })
|
||
|
||
const goTo = (n: number) => {
|
||
setEditing(null)
|
||
setIndex(n)
|
||
}
|
||
|
||
function edit(e: Entry) {
|
||
setEditing(e)
|
||
formRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||
}
|
||
|
||
const catalogFailed = [categories, methods].find((q) => q.isError)
|
||
const catalog = categories.data && methods.data ? { c: categories.data, m: methods.data } : null
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
<div className="mx-auto max-w-3xl lg:max-w-none">
|
||
<WeekPicker
|
||
weeks={period.weeks}
|
||
index={week.index}
|
||
todayIndex={todayIndex}
|
||
budget={budget.data}
|
||
onChange={goTo}
|
||
/>
|
||
</div>
|
||
{readOnly && (
|
||
<p className="flex items-start gap-2 rounded-xl bg-muted px-3 py-2 text-sm text-muted-foreground">
|
||
<Info className="mt-0.5 size-4 shrink-0" aria-hidden />
|
||
{t.ledger.readOnly}
|
||
</p>
|
||
)}
|
||
<div className="grid items-start gap-4 lg:grid-cols-2">
|
||
{!readOnly && (
|
||
<div ref={formRef} className="scroll-mt-4 lg:sticky lg:top-4">
|
||
<Section title={editing ? t.ledger.editTitle : t.ledger.formTitle}>
|
||
{catalogFailed?.error ? (
|
||
<PageError message={catalogFailed.error.message} onRetry={catalogFailed.refetch} />
|
||
) : !catalog ? (
|
||
<PageLoading />
|
||
) : (
|
||
<EntryForm
|
||
key={editing?.id ?? 'new'}
|
||
period={period}
|
||
week={week}
|
||
today={today}
|
||
categories={catalog.c}
|
||
methods={catalog.m}
|
||
entry={editing ?? undefined}
|
||
autoFocus={focusAmount}
|
||
onSaved={(date) => goTo(weekIndexFor(period.weeks, date))}
|
||
onCancel={() => setEditing(null)}
|
||
/>
|
||
)}
|
||
</Section>
|
||
</div>
|
||
)}
|
||
<div className={readOnly ? 'space-y-4 lg:col-span-2' : 'space-y-4'}>
|
||
<Section title={t.ledger.summaryTitle}>
|
||
{budget.isError ? (
|
||
<PageError message={budget.error.message} onRetry={budget.refetch} />
|
||
) : budget.data ? (
|
||
<WeekSummary budget={budget.data} index={week.index} />
|
||
) : (
|
||
<PageLoading />
|
||
)}
|
||
</Section>
|
||
<Section title={t.ledger.listTitle}>
|
||
{entries.isError ? (
|
||
<PageError message={entries.error.message} onRetry={entries.refetch} />
|
||
) : entries.data && catalog ? (
|
||
<EntryList
|
||
periodId={period.id}
|
||
entries={entries.data}
|
||
categories={catalog.c}
|
||
methods={catalog.m}
|
||
editingId={editing?.id}
|
||
readOnly={readOnly}
|
||
onEdit={edit}
|
||
onDeleted={(id) => editing?.id === id && setEditing(null)}
|
||
/>
|
||
) : (
|
||
<PageLoading />
|
||
)}
|
||
</Section>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|