247 lines
8.2 KiB
TypeScript
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>
|
|
);
|
|
}
|