whisper-money/resources/js/components/accounts/update-balance-dialog.tsx

222 lines
7.5 KiB
TypeScript

import {
index,
store,
} from '@/actions/App/Http/Controllers/AccountBalanceController';
import { AmountInput } from '@/components/ui/amount-input';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import type { Account, AccountBalance } from '@/types/account';
import { __ } from '@/utils/i18n';
import { useEffect, useRef, useState } from 'react';
interface UpdateBalanceDialogProps {
account: Account;
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess?: () => void;
}
function getTodayDate(): string {
const today = new Date();
return today.toISOString().split('T')[0];
}
interface PaginatedBalanceResponse {
data: AccountBalance[];
}
export function UpdateBalanceDialog({
account,
open,
onOpenChange,
onSuccess,
}: UpdateBalanceDialogProps) {
const [date, setDate] = useState(getTodayDate());
const [balance, setBalance] = useState(0);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isLoadingLastBalance, setIsLoadingLastBalance] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
async function fetchLastBalance() {
if (!open) return;
setIsLoadingLastBalance(true);
try {
const response = await fetch(
index.url(account.id, { query: { page: '1' } }),
{
headers: {
Accept: 'application/json',
},
},
);
if (!response.ok) {
throw new Error('Failed to fetch last balance');
}
const data: PaginatedBalanceResponse = await response.json();
if (data.data.length > 0) {
setBalance(data.data[0].balance);
} else {
setBalance(0);
}
} catch (err) {
console.error('Failed to fetch last balance:', err);
setBalance(0);
} finally {
setIsLoadingLastBalance(false);
}
}
if (open) {
setDate(getTodayDate());
setError(null);
fetchLastBalance();
}
}, [open, account.id]);
useEffect(() => {
if (open && !isLoadingLastBalance && inputRef.current) {
setTimeout(() => {
const input = inputRef.current;
if (input) {
input.focus();
// Use requestAnimationFrame to ensure selection happens after focus events
requestAnimationFrame(() => {
input.setSelectionRange(0, input.value.length);
});
}
}, 100);
}
}, [open, isLoadingLastBalance]);
function handleOpenChange(newOpen: boolean) {
onOpenChange(newOpen);
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setIsSubmitting(true);
setError(null);
try {
const response = await fetch(store.url(account.id), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': decodeURIComponent(
document.cookie
.split('; ')
.find((row) => row.startsWith('XSRF-TOKEN='))
?.split('=')[1] || '',
),
Accept: 'application/json',
},
body: JSON.stringify({
balance_date: date,
balance: balance,
}),
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.message || 'Failed to update balance');
}
handleOpenChange(false);
onSuccess?.();
} catch (err) {
setError(
err instanceof Error ? err.message : 'Failed to update balance',
);
} finally {
setIsSubmitting(false);
}
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent hasKeyboard className="sm:max-w-[400px]">
<DialogHeader>
<DialogTitle>{__('Update balance')}</DialogTitle>
<DialogDescription>
{__(
'Set the balance for this account on a specific date.',
)}
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="balance-amount">{__('Balance')}</Label>
{isLoadingLastBalance ? (
<div className="flex h-10 items-center rounded-md border border-input bg-muted px-3 text-sm text-muted-foreground">
{__('Loading last balance...')}
</div>
) : (
<AmountInput
ref={inputRef}
id="balance-amount"
className="mt-1"
value={balance}
onChange={setBalance}
currencyCode={account.currency_code}
required
/>
)}
</div>
<div className="space-y-2">
<Label htmlFor="balance-date">{__('Date')}</Label>
<Input
id="balance-date"
type="date"
className="mt-1"
value={date}
onChange={(e) => setDate(e.target.value)}
required
/>
</div>
{error && (
<p className="text-sm text-destructive">{error}</p>
)}
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => handleOpenChange(false)}
disabled={isSubmitting || isLoadingLastBalance}
>
{__('Cancel')}
</Button>
<Button
type="submit"
disabled={isSubmitting || isLoadingLastBalance}
>
{isSubmitting
? __('Saving...')
: isLoadingLastBalance
? __('Loading...')
: __('Save')}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}