Files
kakeibo/frontend/src/features/ledger/page.tsx
T

146 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
}