Files
kakeibo/frontend/src/features/ledger/entry-list.tsx
T
suphicakir 233907e3d9 feat(ekstre): banka/kart ekstresi içe aktarma ekranı
- Ayarlar → Ekstre içe aktar: Excel (.xlsx/.xls) ve CSV, dosya tarayıcıda okunur
- Başlık satırı ve sütunlar otomatik bulunur; tek sütun / Borç-Alacak; eşleştirme profil olarak saklanır
- Gözden geçirme: tür + kategori önerisi, aynı açıklamalara otomatik yayılma, tekrar/engelli satırlar
- Öğrenilen kurallarda ilk kelime yedeği (MIGROS İSTANBUL → MIGROS KADIKÖY)
- Defterde iade etiketi; iade düzenlemede eksi kalır
- SheetJS resmi paketi frontend/vendor altında (npm 12 uzak paket indirmeyi engeller)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 08:58:04 +03:00

156 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 { Pencil, Trash2 } from 'lucide-react'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { useDeleteEntry, useRestoreEntry } from '@/lib/entries'
import { formatDate, formatMoney, formatWeekday } from '@/lib/format'
import { group } from '@/lib/groups'
import { groupByDay } from '@/lib/ledger'
import { t } from '@/lib/strings'
import { cn } from '@/lib/utils'
import type { CatalogItem } from '@/types/CatalogItem'
import type { Entry } from '@/types/Entry'
const s = t.ledger
/**
* Haftanın girişleri, gün gün. Sil → bildirimde "Geri al" (sunucuda soft delete + restore).
* Kategori ve ödeme türü adları pasifler dahil listeden bulunur (eski kayıtlar da adıyla görünür).
*/
export function EntryList({
periodId,
entries,
categories,
methods,
editingId,
readOnly = false,
onEdit,
onDeleted,
}: {
periodId: string
entries: Entry[]
categories: CatalogItem[]
methods: CatalogItem[]
editingId?: string
/** Kapalı dönem: düzenle/sil yok */
readOnly?: boolean
onEdit: (e: Entry) => void
onDeleted: (id: string) => void
}) {
const del = useDeleteEntry(periodId)
const restore = useRestoreEntry(periodId)
const catName = (id: string) => categories.find((c) => c.id === id)
const pmName = (id: string | null) => (id ? methods.find((m) => m.id === id)?.name : undefined)
function remove(e: Entry, name: string) {
del.mutate(e.id, {
onSuccess: () => {
onDeleted(e.id)
toast(s.deleted, {
description: `${formatMoney(e.amount)} · ${name}`,
duration: 8000,
action: {
label: s.undo,
onClick: () =>
restore.mutate(e.id, {
onSuccess: () => toast.success(s.restored),
onError: (err) => toast.error(err.message),
}),
},
})
},
onError: (err) => toast.error(err.message),
})
}
if (entries.length === 0) {
return <p className="text-sm text-muted-foreground">{s.empty}</p>
}
return (
<div className="space-y-4">
{groupByDay(entries).map((day) => (
<section key={day.date} aria-label={`${formatWeekday(day.date)} ${formatDate(day.date)}`}>
<div className="mb-1 flex items-baseline justify-between gap-2 border-b pb-1 text-sm">
<h4 className="font-medium">
{formatWeekday(day.date)}{' '}
<span className="text-muted-foreground">{formatDate(day.date)}</span>
</h4>
<span className="money text-muted-foreground" title={s.dayTotal}>
{formatMoney(day.total)}
</span>
</div>
<ul className="divide-y">
{day.entries.map((e) => {
const g = group(e.group)
const cat = catName(e.category_id)
const name = cat?.name ?? '—'
const editing = e.id === editingId
const details = [g.label, pmName(e.payment_method_id), e.description].filter(Boolean)
return (
<li
key={e.id}
className={cn(
'flex items-center gap-2 py-2',
editing && '-mx-2 rounded-lg bg-muted px-2',
)}
>
<span
aria-hidden
className={cn(
'flex size-9 shrink-0 items-center justify-center rounded-full text-lg',
g.className.soft,
)}
>
{cat?.emoji ?? g.emoji}
</span>
<div className="min-w-0 flex-1">
<p className="truncate font-medium">
{name}
{editing && (
<span className="ml-2 text-xs font-normal text-muted-foreground">
· {s.editing}
</span>
)}
</p>
<p className="truncate text-xs text-muted-foreground">
<span className={g.className.text}>{details[0]}</span>
{details.slice(1).map((d) => ` · ${d}`)}
</p>
</div>
<span className={cn('money shrink-0 font-medium', e.amount < 0 && 'text-income')}>
{e.amount < 0 && <span className="mr-1 text-xs font-normal">{s.refund}</span>}
{formatMoney(e.amount)}
</span>
{!readOnly && (
<div className="flex shrink-0">
<Button
variant="ghost"
size="icon"
aria-label={s.edit(name)}
aria-pressed={editing}
onClick={() => onEdit(e)}
>
<Pencil aria-hidden />
</Button>
<Button
variant="ghost"
size="icon"
aria-label={s.remove(name)}
className="text-muted-foreground hover:text-expense"
disabled={del.isPending && del.variables === e.id}
onClick={() => remove(e, name)}
>
<Trash2 aria-hidden />
</Button>
</div>
)}
</li>
)
})}
</ul>
</section>
))}
</div>
)
}