233907e3d9
- 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>
156 lines
5.5 KiB
TypeScript
156 lines
5.5 KiB
TypeScript
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>
|
||
)
|
||
}
|