feat: remove OTP gate from transactions, fix categories auth, add implementation plan

- Remove OtpGateGuard from transactions controller (OTP verified at login)
- Fix categories controller to use authenticated user instead of TEMP_USER_ID
- Add comprehensive implementation plan document
- Update .env.example with WEB_APP_URL
- Prepare for admin dashboard development
This commit is contained in:
dwindown
2025-10-11 14:00:11 +07:00
parent 0da6071eb3
commit 249f3a9d7d
159 changed files with 13748 additions and 3369 deletions

View File

@@ -18,7 +18,8 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Plus, Search, Edit, Trash2, Wallet } from "lucide-react"
import { Plus, Search, Edit, Trash2, Wallet, Filter, X } from "lucide-react"
import { Label } from "@/components/ui/label"
import axios from "axios"
import { WalletDialog } from "@/components/dialogs/WalletDialog"
import {
@@ -50,7 +51,9 @@ export function Wallets() {
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)
@@ -90,14 +93,21 @@ export function Wallets() {
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 matchesCurrency = currencyFilter === "all" || wallet.currency === currencyFilter
return matchesSearch && matchesCurrency
const matchesKind = kindFilter === "all" || wallet.kind === kindFilter
const matchesCurrency = currencyFilter === "all" || wallet.currency === currencyFilter || wallet.unit === currencyFilter
return matchesSearch && matchesKind && matchesCurrency
})
}, [wallets, searchTerm, currencyFilter])
}, [wallets, searchTerm, kindFilter, currencyFilter])
// Get unique currencies for filter
const availableCurrencies = useMemo(() => {
@@ -147,6 +157,10 @@ export function Wallets() {
</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" />
Add Wallet
@@ -196,46 +210,113 @@ export function Wallets() {
</div>
{/* Filters */}
<Card>
<CardHeader>
<CardTitle>Filters</CardTitle>
{showFilters && (
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-base">Filters</CardTitle>
<Button
variant="ghost"
size="sm"
onClick={clearFilters}
className="h-8 text-xs"
>
<X className="h-3 w-3 mr-1" />
Clear All
</Button>
</div>
</CardHeader>
<CardContent>
<div className="flex gap-4">
<div className="flex-1">
<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">Search Wallet</Label>
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search wallets..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8"
className="pl-9 h-9"
/>
</div>
</div>
<Select value={currencyFilter} onValueChange={setCurrencyFilter}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Filter by currency" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Currencies</SelectItem>
{availableCurrencies.map(currency => (
<SelectItem key={currency} value={currency}>
{currency}
</SelectItem>
))}
</SelectContent>
</Select>
{/* Type Filter */}
<div className="space-y-2">
<Label className="text-xs font-medium text-muted-foreground">Type</Label>
<Select value={kindFilter} onValueChange={setKindFilter}>
<SelectTrigger className="h-9">
<SelectValue placeholder="All types" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types</SelectItem>
<SelectItem value="money">Money</SelectItem>
<SelectItem value="asset">Asset</SelectItem>
</SelectContent>
</Select>
</div>
{/* Currency Filter */}
<div className="space-y-2">
<Label className="text-xs font-medium text-muted-foreground">Currency/Unit</Label>
<Select value={currencyFilter} onValueChange={setCurrencyFilter}>
<SelectTrigger className="h-9">
<SelectValue placeholder="All currencies" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Currencies/Units</SelectItem>
{availableCurrencies.map(currency => (
<SelectItem key={currency} value={currency}>
{currency}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
</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>Wallets ({filteredWallets.length})</CardTitle>
<CardDescription>
{searchTerm || currencyFilter !== "all"
{filteredWallets.length !== wallets.length
? `Filtered from ${wallets.length} total wallets`
: "All your wallets"
}
@@ -246,8 +327,8 @@ export function Wallets() {
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Type</TableHead>
<TableHead>Currency/Unit</TableHead>
<TableHead>Type</TableHead>
<TableHead>Created</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
@@ -256,7 +337,7 @@ export function Wallets() {
{filteredWallets.length === 0 ? (
<TableRow>
<TableCell colSpan={5} className="text-center py-8">
{searchTerm || currencyFilter !== "all"
{filteredWallets.length !== wallets.length
? "No wallets match your filters"
: "No wallets found. Create your first wallet!"
}
@@ -266,6 +347,13 @@ export function Wallets() {
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"
@@ -274,13 +362,6 @@ export function Wallets() {
{wallet.kind}
</Badge>
</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>
{new Date(wallet.createdAt).toLocaleDateString()}
</TableCell>