1- import { useQuery } from "@tanstack/react-query" ;
1+ import { useQuery , useQueryClient } from "@tanstack/react-query" ;
22import { useLocation , useRoute } from "wouter" ;
33import { apiClient } from "../lib/apiClient" ;
44import { useAuth } from "../hooks/useAuth" ;
@@ -15,12 +15,17 @@ export default function CurrencyDetail() {
1515 const [ , params ] = useRoute ( "/currency/:currencyId" ) ;
1616 const [ , setLocation ] = useLocation ( ) ;
1717 const { user } = useAuth ( ) ;
18+ const queryClient = useQueryClient ( ) ;
1819 const [ transferOpen , setTransferOpen ] = useState ( false ) ;
1920 const [ mintOpen , setMintOpen ] = useState ( false ) ;
2021 const [ selectedTransactionId , setSelectedTransactionId ] = useState < string | null > ( null ) ;
2122 const [ transactionOffset , setTransactionOffset ] = useState ( 0 ) ;
2223 const [ allTransactions , setAllTransactions ] = useState < any [ ] > ( [ ] ) ;
2324 const PAGE_SIZE = 10 ;
25+ const MAX_NEGATIVE_SLIDER = 1_000_000 ;
26+ const [ maxNegativeInput , setMaxNegativeInput ] = useState < string > ( "" ) ;
27+ const [ maxNegativeSaving , setMaxNegativeSaving ] = useState ( false ) ;
28+ const [ maxNegativeError , setMaxNegativeError ] = useState < string | null > ( null ) ;
2429
2530 // Load account context from localStorage
2631 const [ accountContext , setAccountContext ] = useState < { type : "user" | "group" ; id : string } | null > ( ( ) => {
@@ -48,6 +53,16 @@ export default function CurrencyDetail() {
4853 enabled : ! ! currencyId ,
4954 } ) ;
5055
56+ useEffect ( ( ) => {
57+ if ( currency ) {
58+ if ( currency . maxNegativeBalance !== null && currency . maxNegativeBalance !== undefined ) {
59+ setMaxNegativeInput ( Math . abs ( Number ( currency . maxNegativeBalance ) ) . toString ( ) ) ;
60+ } else {
61+ setMaxNegativeInput ( "" ) ;
62+ }
63+ }
64+ } , [ currency ] ) ;
65+
5166 const { data : accountDetails } = useQuery ( {
5267 queryKey : [ "accountDetails" , currencyId , accountContext ] ,
5368 queryFn : async ( ) => {
@@ -177,6 +192,40 @@ export default function CurrencyDetail() {
177192
178193 const isAdminOfCurrency = currency && groups ?. some ( ( g : any ) => g . id === currency . groupId && g . isAdmin ) ;
179194
195+ const saveMaxNegative = async ( ) => {
196+ if ( ! currencyId ) return ;
197+ setMaxNegativeError ( null ) ;
198+ setMaxNegativeSaving ( true ) ;
199+ try {
200+ const trimmed = maxNegativeInput . trim ( ) ;
201+ const isClearing = trimmed === "" ;
202+ let payloadValue : number | null = null ;
203+
204+ if ( ! isClearing ) {
205+ const magnitude = parseFloat ( trimmed ) ;
206+ if ( Number . isNaN ( magnitude ) || magnitude < 0 ) {
207+ setMaxNegativeError ( "Enter a valid non-negative number." ) ;
208+ setMaxNegativeSaving ( false ) ;
209+ return ;
210+ }
211+ // Store as negative (or zero)
212+ payloadValue = magnitude === 0 ? 0 : - Math . abs ( magnitude ) ;
213+ }
214+
215+ await apiClient . patch ( `/api/currencies/${ currencyId } /max-negative` , {
216+ value : payloadValue ,
217+ } ) ;
218+
219+ await queryClient . invalidateQueries ( { queryKey : [ "currency" , currencyId ] } ) ;
220+ await queryClient . invalidateQueries ( { queryKey : [ "accountDetails" , currencyId , accountContext ] } ) ;
221+ } catch ( error : any ) {
222+ const message = error ?. response ?. data ?. error || error ?. message || "Failed to update max negative balance" ;
223+ setMaxNegativeError ( message ) ;
224+ } finally {
225+ setMaxNegativeSaving ( false ) ;
226+ }
227+ } ;
228+
180229 if ( ! currencyId ) {
181230 return < div > Currency not found</ div > ;
182231 }
@@ -240,6 +289,16 @@ export default function CurrencyDetail() {
240289 { currency . allowNegative ? "Yes" : "No" }
241290 </ p >
242291 </ div >
292+ < div >
293+ < h3 className = "text-sm font-medium text-muted-foreground mb-1" > Max Negative Balance</ h3 >
294+ < p className = "text-lg font-medium" >
295+ { currency . allowNegative
296+ ? ( currency . maxNegativeBalance !== null && currency . maxNegativeBalance !== undefined
297+ ? Number ( currency . maxNegativeBalance ) . toLocaleString ( )
298+ : "No cap" )
299+ : "Not applicable" }
300+ </ p >
301+ </ div >
243302 < div >
244303 < h3 className = "text-sm font-medium text-muted-foreground mb-1" > Total Currency Supply</ h3 >
245304 < p className = "text-lg font-semibold" >
@@ -257,6 +316,66 @@ export default function CurrencyDetail() {
257316 </ div >
258317 ) }
259318
319+ { /* Max Negative Control - only for admins when negatives are allowed */ }
320+ { currency && currency . allowNegative && isAdminOfCurrency && accountContext ?. type === "group" && accountContext . id === currency . groupId && (
321+ < div className = "bg-white border rounded-lg p-6 mb-6" >
322+ < h3 className = "text-lg font-semibold mb-2" > Set max negative balance</ h3 >
323+ < p className = "text-sm text-muted-foreground mb-4" >
324+ Limit how far any account can go negative for this currency. Leave blank for no cap.
325+ </ p >
326+ < div className = "space-y-4" >
327+ < input
328+ type = "range"
329+ min = { 0 }
330+ max = { MAX_NEGATIVE_SLIDER }
331+ step = { 0.01 }
332+ value = { maxNegativeInput === "" ? 0 : Math . min ( MAX_NEGATIVE_SLIDER , Math . max ( 0 , Number ( maxNegativeInput ) || 0 ) ) }
333+ onChange = { ( e ) => setMaxNegativeInput ( e . target . value ) }
334+ className = "w-full"
335+ />
336+ < div className = "flex flex-col gap-3 md:flex-row md:items-center" >
337+ < div className = "flex-1" >
338+ < label className = "block text-sm font-medium mb-1" > Max negative (absolute value)</ label >
339+ < input
340+ type = "number"
341+ min = { 0 }
342+ max = { MAX_NEGATIVE_SLIDER }
343+ step = { 0.01 }
344+ value = { maxNegativeInput }
345+ onChange = { ( e ) => setMaxNegativeInput ( e . target . value ) }
346+ placeholder = "Leave blank for no cap"
347+ className = "w-full px-4 py-2 border rounded-lg"
348+ />
349+ < div className = "text-xs text-muted-foreground mt-1" >
350+ Saved as negative value: { maxNegativeInput === "" ? "No cap" : `-${ Math . abs ( Number ( maxNegativeInput ) || 0 ) . toLocaleString ( ) } ` }
351+ </ div >
352+ </ div >
353+ < div className = "flex gap-2" >
354+ < button
355+ onClick = { saveMaxNegative }
356+ disabled = { maxNegativeSaving }
357+ className = "px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:opacity-90 disabled:opacity-50"
358+ >
359+ { maxNegativeSaving ? "Saving..." : "Save" }
360+ </ button >
361+ < button
362+ onClick = { ( ) => setMaxNegativeInput ( "" ) }
363+ disabled = { maxNegativeSaving }
364+ className = "px-4 py-2 border rounded-lg hover:bg-gray-50 disabled:opacity-50"
365+ >
366+ Clear cap
367+ </ button >
368+ </ div >
369+ </ div >
370+ { maxNegativeError && (
371+ < div className = "bg-red-50 border border-red-200 text-red-700 px-4 py-2 rounded-lg" >
372+ { maxNegativeError }
373+ </ div >
374+ ) }
375+ </ div >
376+ </ div >
377+ ) }
378+
260379 { /* Transactions */ }
261380 < div className = "mb-6" >
262381 < div className = "flex justify-between items-center mb-4" >
0 commit comments