- Add useLanguage hook to Wallets page - Translate all UI text (buttons, labels, table headers) - Translate filter options and placeholders - Translate delete confirmation dialog - Support both Indonesian and English
418 lines
16 KiB
TypeScript
418 lines
16 KiB
TypeScript
import { useState, useEffect, useMemo } from "react"
|
|
import { useLanguage } from "@/contexts/LanguageContext"
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow
|
|
} from "@/components/ui/table"
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select"
|
|
import { Plus, Search, Edit, Trash2, Wallet, Filter, X } from "lucide-react"
|
|
import { Label } from "@/components/ui/label"
|
|
import axios from "axios"
|
|
import { toast } from "sonner"
|
|
import { WalletDialog } from "@/components/dialogs/WalletDialog"
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
AlertDialogTrigger,
|
|
} from "@/components/ui/alert-dialog"
|
|
|
|
interface Wallet {
|
|
id: string
|
|
name: string
|
|
kind: "money" | "asset"
|
|
currency?: string | null
|
|
unit?: string | null
|
|
deletedAt?: string | null
|
|
createdAt: string
|
|
updatedAt: string
|
|
}
|
|
|
|
const API = "/api"
|
|
|
|
export function Wallets() {
|
|
const { t } = useLanguage()
|
|
const [wallets, setWallets] = useState<Wallet[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [searchTerm, setSearchTerm] = useState("")
|
|
const [kindFilter, setKindFilter] = useState<string>("all")
|
|
const [currencyFilter, setCurrencyFilter] = useState<string>("all")
|
|
const [showFilters, setShowFilters] = useState(false)
|
|
const [walletDialogOpen, setWalletDialogOpen] = useState(false)
|
|
const [editingWallet, setEditingWallet] = useState<Wallet | null>(null)
|
|
|
|
useEffect(() => {
|
|
loadWallets()
|
|
}, [])
|
|
|
|
const loadWallets = async () => {
|
|
try {
|
|
setLoading(true)
|
|
const response = await axios.get(`${API}/wallets`)
|
|
setWallets(response.data.filter((w: Wallet) => !w.deletedAt))
|
|
} catch (error) {
|
|
console.error('Failed to load wallets:', error)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const deleteWallet = async (id: string) => {
|
|
try {
|
|
await axios.delete(`${API}/wallets/${id}`)
|
|
toast.success('Wallet berhasil dihapus')
|
|
await loadWallets()
|
|
} catch (error) {
|
|
console.error('Failed to delete wallet:', error)
|
|
toast.error('Gagal menghapus wallet')
|
|
}
|
|
}
|
|
|
|
const handleEditWallet = (wallet: Wallet) => {
|
|
setEditingWallet(wallet)
|
|
setWalletDialogOpen(true)
|
|
}
|
|
|
|
const handleDialogClose = () => {
|
|
setWalletDialogOpen(false)
|
|
setEditingWallet(null)
|
|
}
|
|
|
|
const clearFilters = () => {
|
|
setSearchTerm("")
|
|
setKindFilter("all")
|
|
setCurrencyFilter("all")
|
|
}
|
|
|
|
// Filter wallets
|
|
const filteredWallets = useMemo(() => {
|
|
return wallets.filter(wallet => {
|
|
const matchesSearch = wallet.name.toLowerCase().includes(searchTerm.toLowerCase())
|
|
const matchesKind = kindFilter === "all" || wallet.kind === kindFilter
|
|
const matchesCurrency = currencyFilter === "all" || wallet.currency === currencyFilter || wallet.unit === currencyFilter
|
|
return matchesSearch && matchesKind && matchesCurrency
|
|
})
|
|
}, [wallets, searchTerm, kindFilter, currencyFilter])
|
|
|
|
// Get unique currencies for filter
|
|
const availableCurrencies = useMemo(() => {
|
|
const currencies = new Set(wallets.map(w => w.currency).filter(Boolean) as string[])
|
|
return Array.from(currencies).sort()
|
|
}, [wallets])
|
|
|
|
// Calculate stats
|
|
const stats = useMemo(() => {
|
|
const totalWallets = filteredWallets.length
|
|
const moneyWallets = filteredWallets.filter(w => w.kind === 'money').length
|
|
const assetWallets = filteredWallets.filter(w => w.kind === 'asset').length
|
|
const currencyCount = new Set(filteredWallets.map(w => w.currency).filter(Boolean)).size
|
|
|
|
return { totalWallets, moneyWallets, assetWallets, currencyCount }
|
|
}, [filteredWallets])
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="h-8 w-48 bg-gray-200 rounded animate-pulse" />
|
|
<div className="grid gap-4 grid-cols-2 lg:grid-cols-4">
|
|
{[...Array(4)].map((_, i) => (
|
|
<Card key={i}>
|
|
<CardHeader>
|
|
<div className="h-4 w-20 bg-gray-200 rounded animate-pulse" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="h-8 w-16 bg-gray-200 rounded animate-pulse" />
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="space-y-4">
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-3xl font-bold tracking-tight">Wallets</h1>
|
|
<p className="text-muted-foreground">
|
|
Manage your wallets and accounts
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-2 sm:flex-shrink-0">
|
|
<Button variant="outline" onClick={() => setShowFilters(!showFilters)}>
|
|
<Filter className="mr-2 h-4 w-4" />
|
|
{showFilters ? 'Hide Filters' : 'Show Filters'}
|
|
</Button>
|
|
<Button onClick={() => setWalletDialogOpen(true)}>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
{t.wallets.addWallet}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats Cards */}
|
|
<div className="grid gap-4 grid-cols-2 lg:grid-cols-4">
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">{t.wallets.title}</CardTitle>
|
|
<Wallet className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{stats.totalWallets}</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">{t.wallets.moneyWallets}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{stats.moneyWallets}</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">{t.wallets.assetWallets}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{stats.assetWallets}</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">{t.wallets.currency}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{stats.currencyCount}</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
{showFilters && (
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle className="text-base">{t.common.filter}</CardTitle>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={clearFilters}
|
|
className="h-7 px-2"
|
|
>
|
|
<X className="h-3 w-3 mr-1" />
|
|
Clear All
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{/* Row 1: Search, Type, Currency */}
|
|
<div className="grid gap-3 md:grid-cols-3">
|
|
{/* Search */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium text-muted-foreground">{t.common.search}</Label>
|
|
<div className="relative">
|
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder={t.wallets.searchPlaceholder}
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="pl-9 h-9"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Type Filter */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium text-muted-foreground">{t.wallets.type}</Label>
|
|
<Select value={kindFilter} onValueChange={setKindFilter}>
|
|
<SelectTrigger className="h-9">
|
|
<SelectValue placeholder={t.common.all} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">{t.common.all}</SelectItem>
|
|
<SelectItem value="money">{t.wallets.money}</SelectItem>
|
|
<SelectItem value="asset">{t.wallets.asset}</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Currency Filter */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium text-muted-foreground">{t.wallets.currency}/{t.wallets.unit}</Label>
|
|
<Select value={currencyFilter} onValueChange={setCurrencyFilter}>
|
|
<SelectTrigger className="h-9">
|
|
<SelectValue placeholder={t.common.all} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">{t.common.all}</SelectItem>
|
|
{availableCurrencies.map(currency => (
|
|
<SelectItem key={currency} value={currency}>
|
|
{currency}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Active Filters Badge */}
|
|
{(searchTerm || kindFilter !== "all" || currencyFilter !== "all") && (
|
|
<div className="flex flex-wrap gap-2">
|
|
{searchTerm && (
|
|
<div className="inline-flex items-center gap-1 px-2.5 py-1 bg-primary/10 text-primary rounded-md text-xs">
|
|
<Search className="h-3 w-3" />
|
|
{searchTerm}
|
|
<button onClick={() => setSearchTerm("")} className="ml-1 hover:bg-primary/20 rounded-full p-0.5">
|
|
<X className="h-3 w-3" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
{kindFilter !== "all" && (
|
|
<div className="inline-flex items-center gap-1 px-2.5 py-1 bg-primary/10 text-primary rounded-md text-xs">
|
|
Type: {kindFilter === "money" ? "Money" : "Asset"}
|
|
<button onClick={() => setKindFilter("all")} className="ml-1 hover:bg-primary/20 rounded-full p-0.5">
|
|
<X className="h-3 w-3" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
{currencyFilter !== "all" && (
|
|
<div className="inline-flex items-center gap-1 px-2.5 py-1 bg-primary/10 text-primary rounded-md text-xs">
|
|
Currency: {currencyFilter}
|
|
<button onClick={() => setCurrencyFilter("all")} className="ml-1 hover:bg-primary/20 rounded-full p-0.5">
|
|
<X className="h-3 w-3" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Wallets Table */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{t.wallets.title} ({filteredWallets.length})</CardTitle>
|
|
<CardDescription>
|
|
{filteredWallets.length !== wallets.length
|
|
? `Filtered from ${wallets.length} total wallets`
|
|
: "All your wallets"
|
|
}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="px-0">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>{t.wallets.name}</TableHead>
|
|
<TableHead>{t.wallets.currency}/{t.wallets.unit}</TableHead>
|
|
<TableHead>{t.wallets.type}</TableHead>
|
|
<TableHead>{t.common.date}</TableHead>
|
|
<TableHead className="text-right">{t.common.actions}</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filteredWallets.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell colSpan={5} className="text-center py-8">
|
|
{filteredWallets.length !== wallets.length
|
|
? t.wallets.noWallets
|
|
: t.wallets.createFirst
|
|
}
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
filteredWallets.map((wallet) => (
|
|
<TableRow key={wallet.id}>
|
|
<TableCell className="font-medium text-nowrap">{wallet.name}</TableCell>
|
|
<TableCell>
|
|
{wallet.kind === 'money' ? (
|
|
<Badge variant="outline" className="text-nowrap">{wallet.currency}</Badge>
|
|
) : (
|
|
<Badge variant="outline" className="text-nowrap">{wallet.unit}</Badge>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge
|
|
variant="outline"
|
|
className={`text-nowrap ${wallet.kind === 'money' ? 'bg-[var(--chart-1)]/20 text-[var(--chart-1)] ring-1 ring-[var(--chart-1)]' : 'bg-[var(--chart-2)]/20 text-[var(--chart-2)] ring-1 ring-[var(--chart-2)]'}`}
|
|
>
|
|
{wallet.kind}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
{new Date(wallet.createdAt).toLocaleDateString()}
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
<div className="flex justify-end gap-2">
|
|
<Button variant="ghost" size="sm" onClick={() => handleEditWallet(wallet)}>
|
|
<Edit className="h-4 w-4" />
|
|
</Button>
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button variant="ghost" size="sm">
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>{t.common.delete} {t.wallets.title}</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
{t.wallets.deleteConfirm}
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>{t.common.cancel}</AlertDialogCancel>
|
|
<AlertDialogAction onClick={() => deleteWallet(wallet.id)}>
|
|
{t.common.delete}
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Dialog */}
|
|
<WalletDialog
|
|
open={walletDialogOpen}
|
|
onOpenChange={handleDialogClose}
|
|
wallet={editingWallet}
|
|
onSuccess={loadWallets}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|