Fix dark theme fill, theme toggle visibility and bank account creation
- The Carbon theme class is now also set on html, with a themed background and a full-height root, so the dark theme fills the whole window instead of stopping at the content height. An inline script in index.html applies the saved theme before first paint. - The header theme button gets a tooltip, inline padding and a max width so it is no longer clipped at the window edge. New Invoice also gets a labelled Dark mode toggle wired to the same handler. - A shared BankAccountModal replaces the inline editor in Settings and adds a Create bank account button under the bank selector on New Invoice, which selects the new account on save. Settings keeps one Add bank account button under the table. Bank name and account number are now required.
This commit is contained in:
@@ -61,7 +61,11 @@ export function AppShell({ view, onNavigate, theme, onToggleTheme, children }: A
|
||||
))}
|
||||
</HeaderNavigation>
|
||||
<HeaderGlobalBar>
|
||||
<HeaderGlobalAction aria-label="Toggle colour theme" onClick={onToggleTheme}>
|
||||
<HeaderGlobalAction
|
||||
aria-label={theme === "g100" ? "Switch to light theme" : "Switch to dark theme"}
|
||||
tooltipAlignment="end"
|
||||
onClick={onToggleTheme}
|
||||
>
|
||||
{theme === "g100" ? <Light size={20} /> : <Asleep size={20} />}
|
||||
</HeaderGlobalAction>
|
||||
</HeaderGlobalBar>
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import { useEffect, useState, type RefObject } from "react";
|
||||
import { Column, Grid, Modal, TextInput, Toggle } from "@carbon/react";
|
||||
import { api } from "../lib/api";
|
||||
import type { BankAccount } from "../lib/types";
|
||||
import { ifscError } from "../lib/validators";
|
||||
import { useToast } from "./ToastProvider";
|
||||
|
||||
export const blankBank = (): BankAccount => ({
|
||||
id: null,
|
||||
label: "",
|
||||
bankName: "",
|
||||
accountName: "",
|
||||
accountNo: "",
|
||||
branch: "",
|
||||
ifsc: "",
|
||||
isDefault: false,
|
||||
});
|
||||
|
||||
interface BankAccountModalProps {
|
||||
open: boolean;
|
||||
/** The account to edit; null opens a blank one. */
|
||||
bank: BankAccount | null;
|
||||
onClose: () => void;
|
||||
onSaved: (bank: BankAccount) => void;
|
||||
/** Focus returns here when the modal closes. */
|
||||
launcherRef?: RefObject<HTMLElement | null>;
|
||||
}
|
||||
|
||||
export default function BankAccountModal({ open, bank, onClose, onSaved, launcherRef }: BankAccountModalProps) {
|
||||
const toast = useToast();
|
||||
const [form, setForm] = useState<BankAccount>(blankBank());
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [touched, setTouched] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setForm(bank ? { ...bank } : blankBank());
|
||||
setBusy(false);
|
||||
setTouched(false);
|
||||
}, [open, bank]);
|
||||
|
||||
const set = <K extends keyof BankAccount>(key: K, value: BankAccount[K]) =>
|
||||
setForm((prev) => ({ ...prev, [key]: value }));
|
||||
|
||||
const bankNameProblem = form.bankName.trim() ? undefined : "Bank name is required";
|
||||
const accountNoProblem = form.accountNo.trim() ? undefined : "Account number is required";
|
||||
const ifscProblem = ifscError(form.ifsc);
|
||||
const blocking = bankNameProblem ?? accountNoProblem ?? ifscProblem;
|
||||
|
||||
const submit = async () => {
|
||||
setTouched(true);
|
||||
if (blocking) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
const saved = await api.saveBank({
|
||||
...form,
|
||||
bankName: form.bankName.trim(),
|
||||
accountNo: form.accountNo.trim(),
|
||||
ifsc: form.ifsc.trim().toUpperCase(),
|
||||
});
|
||||
toast.success(form.id === null ? "Bank account added" : "Bank account saved", saved.bankName);
|
||||
onSaved(saved);
|
||||
} catch (e) {
|
||||
toast.error("Could not save bank account", String(e));
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
modalHeading={bank?.id ? "Edit bank account" : "New bank account"}
|
||||
primaryButtonText={busy ? "Saving…" : "Save account"}
|
||||
secondaryButtonText="Cancel"
|
||||
primaryButtonDisabled={busy}
|
||||
onRequestClose={onClose}
|
||||
onRequestSubmit={() => void submit()}
|
||||
launcherButtonRef={launcherRef}
|
||||
size="md"
|
||||
>
|
||||
<Grid narrow>
|
||||
<Column sm={4} md={4} lg={8}>
|
||||
<TextInput
|
||||
id="bm-label"
|
||||
labelText="Label (optional)"
|
||||
value={form.label}
|
||||
onChange={(e) => set("label", e.target.value)}
|
||||
/>
|
||||
</Column>
|
||||
<Column sm={4} md={4} lg={8}>
|
||||
<TextInput
|
||||
id="bm-bankName"
|
||||
labelText="Bank name"
|
||||
value={form.bankName}
|
||||
onChange={(e) => set("bankName", e.target.value)}
|
||||
invalid={touched && Boolean(bankNameProblem)}
|
||||
invalidText={bankNameProblem}
|
||||
data-modal-primary-focus
|
||||
/>
|
||||
</Column>
|
||||
<Column sm={4} md={4} lg={8}>
|
||||
<TextInput
|
||||
id="bm-accountName"
|
||||
labelText="Account name"
|
||||
value={form.accountName}
|
||||
onChange={(e) => set("accountName", e.target.value)}
|
||||
/>
|
||||
</Column>
|
||||
<Column sm={4} md={4} lg={8}>
|
||||
<TextInput
|
||||
id="bm-accountNo"
|
||||
labelText="Account number"
|
||||
value={form.accountNo}
|
||||
onChange={(e) => set("accountNo", e.target.value)}
|
||||
invalid={touched && Boolean(accountNoProblem)}
|
||||
invalidText={accountNoProblem}
|
||||
/>
|
||||
</Column>
|
||||
<Column sm={4} md={4} lg={8}>
|
||||
<TextInput
|
||||
id="bm-branch"
|
||||
labelText="Branch"
|
||||
value={form.branch}
|
||||
onChange={(e) => set("branch", e.target.value)}
|
||||
/>
|
||||
</Column>
|
||||
<Column sm={4} md={4} lg={8}>
|
||||
<TextInput
|
||||
id="bm-ifsc"
|
||||
labelText="IFSC"
|
||||
value={form.ifsc}
|
||||
onChange={(e) => set("ifsc", e.target.value.toUpperCase())}
|
||||
invalid={Boolean(ifscProblem)}
|
||||
invalidText={ifscProblem}
|
||||
/>
|
||||
</Column>
|
||||
<Column sm={4} md={8} lg={16}>
|
||||
<Toggle
|
||||
id="bm-default"
|
||||
labelText="Use as default account"
|
||||
toggled={form.isDefault}
|
||||
onToggle={(value) => set("isDefault", value)}
|
||||
/>
|
||||
</Column>
|
||||
</Grid>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user