trade history for NAV fund investments
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m20s
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m20s
This commit is contained in:
@@ -1,10 +1,16 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState, useRef, useEffect, useCallback } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { formatCurrency } from '@/lib/utils'
|
import { formatCurrency } from '@/lib/utils'
|
||||||
import { Building2, Plus, Trash2, UserPlus, UserMinus } from 'lucide-react'
|
import { Building2, Plus, Trash2, UserPlus, UserMinus } from 'lucide-react'
|
||||||
|
|
||||||
|
interface UserSuggestion {
|
||||||
|
id: string
|
||||||
|
username: string
|
||||||
|
displayUsername: string | null
|
||||||
|
}
|
||||||
|
|
||||||
interface Manager {
|
interface Manager {
|
||||||
id: string
|
id: string
|
||||||
userId: string
|
userId: string
|
||||||
@@ -26,9 +32,37 @@ export default function AdminFundActions({ funds: initialFunds }: { funds: Fund[
|
|||||||
const [newName, setNewName] = useState('')
|
const [newName, setNewName] = useState('')
|
||||||
const [newBalance, setNewBalance] = useState('10000')
|
const [newBalance, setNewBalance] = useState('10000')
|
||||||
const [addUsername, setAddUsername] = useState<Record<string, string>>({})
|
const [addUsername, setAddUsername] = useState<Record<string, string>>({})
|
||||||
|
const [suggestions, setSuggestions] = useState<Record<string, UserSuggestion[]>>({})
|
||||||
|
const [showSuggestions, setShowSuggestions] = useState<Record<string, boolean>>({})
|
||||||
const [expandedId, setExpandedId] = useState<string | null>(null)
|
const [expandedId, setExpandedId] = useState<string | null>(null)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
const debounceRef = useRef<Record<string, ReturnType<typeof setTimeout>>>({})
|
||||||
|
|
||||||
|
const fetchSuggestions = useCallback(async (fundId: string, q: string) => {
|
||||||
|
if (!q.trim()) {
|
||||||
|
setSuggestions((s) => ({ ...s, [fundId]: [] }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const res = await fetch(`/api/admin/users?q=${encodeURIComponent(q)}&limit=8`)
|
||||||
|
if (res.ok) {
|
||||||
|
const data: UserSuggestion[] = await res.json()
|
||||||
|
setSuggestions((s) => ({ ...s, [fundId]: data }))
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
function handleUsernameChange(fundId: string, value: string) {
|
||||||
|
setAddUsername((u) => ({ ...u, [fundId]: value }))
|
||||||
|
setShowSuggestions((s) => ({ ...s, [fundId]: true }))
|
||||||
|
clearTimeout(debounceRef.current[fundId])
|
||||||
|
debounceRef.current[fundId] = setTimeout(() => fetchSuggestions(fundId, value), 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectSuggestion(fundId: string, username: string) {
|
||||||
|
setAddUsername((u) => ({ ...u, [fundId]: username }))
|
||||||
|
setShowSuggestions((s) => ({ ...s, [fundId]: false }))
|
||||||
|
setSuggestions((s) => ({ ...s, [fundId]: [] }))
|
||||||
|
}
|
||||||
|
|
||||||
async function createFund() {
|
async function createFund() {
|
||||||
if (!newName.trim()) return
|
if (!newName.trim()) return
|
||||||
@@ -206,17 +240,45 @@ export default function AdminFundActions({ funds: initialFunds }: { funds: Fund[
|
|||||||
|
|
||||||
{/* Add manager */}
|
{/* Add manager */}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
<div className="relative flex-1">
|
||||||
<input
|
<input
|
||||||
value={addUsername[fund.id] ?? ''}
|
value={addUsername[fund.id] ?? ''}
|
||||||
onChange={(e) => setAddUsername({ ...addUsername, [fund.id]: e.target.value })}
|
onChange={(e) => handleUsernameChange(fund.id, e.target.value)}
|
||||||
placeholder="username"
|
placeholder="username"
|
||||||
onKeyDown={(e) => e.key === 'Enter' && addManager(fund.id)}
|
onKeyDown={(e) => {
|
||||||
className="flex-1 bg-surface border border-surface-border rounded-lg px-3 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
if (e.key === 'Enter') { e.preventDefault(); addManager(fund.id) }
|
||||||
|
if (e.key === 'Escape') setShowSuggestions((s) => ({ ...s, [fund.id]: false }))
|
||||||
|
}}
|
||||||
|
onFocus={() => {
|
||||||
|
if ((addUsername[fund.id] ?? '').trim()) {
|
||||||
|
setShowSuggestions((s) => ({ ...s, [fund.id]: true }))
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={() => setTimeout(() => setShowSuggestions((s) => ({ ...s, [fund.id]: false })), 150)}
|
||||||
|
className="w-full bg-surface border border-surface-border rounded-lg px-3 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||||
/>
|
/>
|
||||||
|
{showSuggestions[fund.id] && (suggestions[fund.id] ?? []).length > 0 && (
|
||||||
|
<div className="absolute z-10 top-full mt-1 left-0 right-0 bg-surface-card border border-surface-border rounded-lg shadow-lg overflow-hidden">
|
||||||
|
{suggestions[fund.id].map((u) => (
|
||||||
|
<button
|
||||||
|
key={u.id}
|
||||||
|
type="button"
|
||||||
|
onMouseDown={() => selectSuggestion(fund.id, u.username)}
|
||||||
|
className="w-full text-left px-3 py-2 text-xs hover:bg-indigo-500/10 transition-colors flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<span className="font-medium text-slate-200">{u.username}</span>
|
||||||
|
{u.displayUsername && u.displayUsername !== u.username && (
|
||||||
|
<span className="text-slate-500">{u.displayUsername}</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => addManager(fund.id)}
|
onClick={() => addManager(fund.id)}
|
||||||
disabled={loading || !(addUsername[fund.id] ?? '').trim()}
|
disabled={loading || !(addUsername[fund.id] ?? '').trim()}
|
||||||
className="flex items-center gap-1.5 px-3 py-1.5 bg-emerald-600/20 border border-emerald-500/30 hover:bg-emerald-600/30 text-emerald-300 text-xs rounded-lg disabled:opacity-50 transition-colors"
|
className="flex items-center gap-1.5 px-3 py-1.5 bg-emerald-600/20 border border-emerald-500/30 hover:bg-emerald-600/30 text-emerald-300 text-xs rounded-lg disabled:opacity-50 transition-colors shrink-0"
|
||||||
>
|
>
|
||||||
<UserPlus className="h-3 w-3" /> Add Manager
|
<UserPlus className="h-3 w-3" /> Add Manager
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { getServerSession } from 'next-auth'
|
||||||
|
import { authOptions } from '@/lib/auth'
|
||||||
|
import { prisma } from '@/lib/prisma'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/admin/users?q=<search>&limit=<n>
|
||||||
|
* Returns users matching the query (excludes fund shadow accounts).
|
||||||
|
*/
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
const session = await getServerSession(authOptions)
|
||||||
|
if (!session?.user?.isAdmin) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { searchParams } = req.nextUrl
|
||||||
|
const q = (searchParams.get('q') ?? '').trim().toLowerCase()
|
||||||
|
const limit = Math.min(10, Math.max(1, parseInt(searchParams.get('limit') ?? '8', 10)))
|
||||||
|
|
||||||
|
if (!q) return NextResponse.json([])
|
||||||
|
|
||||||
|
const users = await prisma.user.findMany({
|
||||||
|
where: {
|
||||||
|
isFund: false,
|
||||||
|
username: { contains: q },
|
||||||
|
},
|
||||||
|
select: { id: true, username: true, displayUsername: true },
|
||||||
|
orderBy: { username: 'asc' },
|
||||||
|
take: limit,
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json(users)
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user