feat(defter): haftanın girişleri, düzenle/sil + geri al, haftalık özet
- Girişler gün gün, gün toplamıyla; satırda kategori, grup, ödeme türü, açıklama - Düzenleme aynı formda (sürüm kontrolü, pasif kategori korunur) - Silmede "Geri al" bildirimi; grup bazlı bütçe / harcanan / kalan özeti Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
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,
|
||||
onEdit,
|
||||
onDeleted,
|
||||
}: {
|
||||
periodId: string
|
||||
entries: Entry[]
|
||||
categories: CatalogItem[]
|
||||
methods: CatalogItem[]
|
||||
editingId?: string
|
||||
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="money shrink-0 font-medium">{formatMoney(e.amount)}</span>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user