whisper-money/resources/js/components/accounts/edit-account-dialog.tsx

247 lines
8.2 KiB
TypeScript

import { update } from '@/actions/App/Http/Controllers/Settings/AccountController';
import { store as storeBank } from '@/actions/App/Http/Controllers/Settings/BankController';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { decrypt, encrypt, importKey } from '@/lib/crypto';
import { getStoredKey } from '@/lib/key-storage';
import type { Account } from '@/types/account';
import { __ } from '@/utils/i18n';
import { router } from '@inertiajs/react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { AccountForm, AccountFormData } from './account-form';
interface EditAccountDialogProps {
account: Account;
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess?: () => void;
redirectTo?: string;
}
export function EditAccountDialog({
account,
open,
onOpenChange,
onSuccess,
redirectTo,
}: EditAccountDialogProps) {
const [decryptedName, setDecryptedName] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const formDataRef = useRef<AccountFormData>({
displayName: '',
bankId: account.bank.id,
type: account.type,
currencyCode: account.currency_code,
customBank: null,
});
useEffect(() => {
if (!open) return;
async function decryptName() {
const keyString = getStoredKey();
if (!keyString) {
setDecryptedName('[Encrypted]');
return;
}
try {
const key = await importKey(keyString);
const name = await decrypt(account.name, key, account.name_iv);
setDecryptedName(name);
} catch (err) {
console.error('Failed to decrypt account name:', err);
setDecryptedName('[Encrypted]');
}
}
decryptName();
}, [open, account.name, account.name_iv]);
const initialValues = useMemo(
() =>
decryptedName && decryptedName !== '[Encrypted]'
? {
displayName: decryptedName,
bank: account.bank,
type: account.type,
currencyCode: account.currency_code,
}
: undefined,
[decryptedName, account.bank, account.type, account.currency_code],
);
const handleFormChange = useCallback((data: AccountFormData) => {
formDataRef.current = data;
}, []);
async function createBankAndGetId(): Promise<string | null> {
const customBank = formDataRef.current.customBank;
if (!customBank) return null;
const formData = new FormData();
formData.append('name', customBank.name);
if (customBank.logo) {
formData.append('logo', customBank.logo);
}
try {
const response = await fetch(storeBank.url(), {
method: 'POST',
body: formData,
headers: {
'X-XSRF-TOKEN': decodeURIComponent(
document.cookie
.split('; ')
.find((row) => row.startsWith('XSRF-TOKEN='))
?.split('=')[1] || '',
),
Accept: 'application/json',
},
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Failed to create bank');
}
const data = await response.json();
return data.id;
} catch (err) {
console.error('Failed to create bank:', err);
throw err;
}
}
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const { displayName, bankId, type, currencyCode, customBank } =
formDataRef.current;
const keyString = getStoredKey();
if (!keyString) {
alert('Encryption key not available. Please unlock first.');
return;
}
if (!type || !currencyCode) {
alert('Please fill in all required fields.');
return;
}
setIsSubmitting(true);
try {
let finalBankId: string;
if (customBank) {
if (!customBank.name.trim()) {
alert('Please enter a bank name.');
setIsSubmitting(false);
return;
}
const createdBankId = await createBankAndGetId();
if (!createdBankId) {
throw new Error('Failed to create bank');
}
finalBankId = createdBankId;
} else {
if (!bankId) {
alert('Please select a bank.');
setIsSubmitting(false);
return;
}
finalBankId = String(bankId);
}
const key = await importKey(keyString);
const { encrypted, iv } = await encrypt(displayName, key);
router.patch(
update.url(account.id),
{
name: encrypted,
name_iv: iv,
bank_id: finalBankId,
type: type,
currency_code: currencyCode,
},
{
preserveScroll: true,
onSuccess: () => {
onOpenChange(false);
if (redirectTo) {
router.visit(redirectTo);
} else {
onSuccess?.();
}
},
onFinish: () => {
setIsSubmitting(false);
},
},
);
} catch (err) {
console.error('Submission failed:', err);
alert(
err instanceof Error
? err.message
: 'Failed to update account. Please try again.',
);
setIsSubmitting(false);
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent hasKeyboard className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>{__('Edit Account')}</DialogTitle>
<DialogDescription>
{__('Update the account information.')}
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-2">
{initialValues ? (
<AccountForm
initialValues={initialValues}
onChange={handleFormChange}
/>
) : (
<div className="space-y-4">
<div className="h-10 animate-pulse rounded bg-muted" />
<div className="h-10 animate-pulse rounded bg-muted" />
<div className="h-10 animate-pulse rounded bg-muted" />
<div className="h-10 animate-pulse rounded bg-muted" />
</div>
)}
<div className="flex justify-end gap-2 pt-4">
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isSubmitting}
>
{__('Cancel')}
</Button>
<Button
type="submit"
disabled={isSubmitting || !initialValues}
>
{isSubmitting ? 'Updating...' : 'Update'}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
}