Start the setup wizard blank and add a Run setup again button
The first-run seed no longer inserts the original vendor's name, address, contact details, PAN or bank account; the wizard now starts empty and requires a name and state. Settings > Data gets a Run setup again button that clears the onboarded flag. Re-running the wizard keeps the active invoice series unless the prefix or padding changes. The sample logo moves out of public/ so it no longer ships in the app.
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Button, InlineLoading, InlineNotification, Modal, Tile, Toggle } from "@carbon/react";
|
||||
import { open, save } from "@tauri-apps/plugin-dialog";
|
||||
import { confirm, open, save } from "@tauri-apps/plugin-dialog";
|
||||
import { api } from "../lib/api";
|
||||
import type { Settings } from "../lib/types";
|
||||
import {
|
||||
backupFileName,
|
||||
ensureZipExtension,
|
||||
@@ -20,8 +21,15 @@ type RestoreStep = "confirm" | "staging" | "ready";
|
||||
const joinPath = (dir: string, name: string) =>
|
||||
!dir ? name : /[\\/]$/.test(dir) ? `${dir}${name}` : `${dir}${dir.includes("\\") && !dir.includes("/") ? "\\" : "/"}${name}`;
|
||||
|
||||
/** Settings > Data: manual backup, restore (staged, applied after a restart) and the automatic daily backup. */
|
||||
export default function DataBackupPanel() {
|
||||
interface DataBackupPanelProps {
|
||||
/** The saved settings; "Run setup again" writes them back with the wizard flag cleared. */
|
||||
settings: Settings;
|
||||
/** Called after the flag is cleared, so the app reloads settings and shows the wizard. */
|
||||
onRerunSetup: () => void;
|
||||
}
|
||||
|
||||
/** Settings > Data: manual backup, restore (staged, applied after a restart), the automatic daily backup and re-running setup. */
|
||||
export default function DataBackupPanel({ settings, onRerunSetup }: DataBackupPanelProps) {
|
||||
const toast = useToast();
|
||||
const [status, setStatus] = useState<BackupStatus | null>(null);
|
||||
const [backingUp, setBackingUp] = useState(false);
|
||||
@@ -32,6 +40,20 @@ export default function DataBackupPanel() {
|
||||
const [restarting, setRestarting] = useState(false);
|
||||
useReturnFocus(restorePath !== null);
|
||||
|
||||
const rerunSetup = async () => {
|
||||
const ok = await confirm(
|
||||
"Open the setup wizard again? Your invoices, clients and backups are kept, and the wizard starts with your current details.",
|
||||
{ title: "Run setup again", kind: "warning" },
|
||||
);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await api.saveSettings({ ...settings, onboarded: false });
|
||||
onRerunSetup();
|
||||
} catch (e) {
|
||||
toast.error("Could not start setup", String(e));
|
||||
}
|
||||
};
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
setStatus(await api.getBackupStatus());
|
||||
@@ -229,6 +251,19 @@ export default function DataBackupPanel() {
|
||||
</div>
|
||||
</Tile>
|
||||
|
||||
<Tile>
|
||||
<h2 className="voiced-section-title">Setup wizard</h2>
|
||||
<p className="voiced-muted">
|
||||
Run the first-time setup again to change your business details, logo, bank account and numbering in one go. Existing
|
||||
invoices, clients and backups are not touched, and invoice numbering carries on unless you change the prefix.
|
||||
</p>
|
||||
<div className="voiced-actions">
|
||||
<Button size="md" kind="tertiary" onClick={() => void rerunSetup()}>
|
||||
Run setup again
|
||||
</Button>
|
||||
</div>
|
||||
</Tile>
|
||||
|
||||
<Modal
|
||||
open={restorePath !== null}
|
||||
danger={step === "confirm"}
|
||||
|
||||
+26
-4
@@ -7,7 +7,8 @@ import type { BankAccount, Invoice, InvoiceInput, Settings } from "./types";
|
||||
|
||||
/**
|
||||
* The end-to-end invoice flow through the real IPC, run against the fresh database the self-test
|
||||
* launches with (it seeds the unregistered vendor, a default bank and an active series).
|
||||
* launches with. The fresh database is neutral (no vendor, no bank), so the first step provisions a
|
||||
* fake vendor and bank the way the setup wizard would.
|
||||
*/
|
||||
|
||||
const today = () => {
|
||||
@@ -28,11 +29,32 @@ export async function runE2eSteps(log: StepLog, reportPath: string): Promise<voi
|
||||
let bank!: BankAccount;
|
||||
let invoice: Invoice | null = null;
|
||||
|
||||
const loaded = await log.run("e2e: get settings and default bank", async () => {
|
||||
const loaded = await log.run("e2e: provision vendor and bank, get settings", async () => {
|
||||
s = await api.getSettings();
|
||||
const banks = await api.listBanks();
|
||||
let banks = await api.listBanks();
|
||||
if (banks.length === 0) {
|
||||
const saved = await api.saveBank({
|
||||
id: null,
|
||||
label: "Primary",
|
||||
bankName: "Self-test Bank",
|
||||
accountName: "Self-test Vendor",
|
||||
accountNo: "000000000000",
|
||||
branch: "000001",
|
||||
ifsc: "TEST0000001",
|
||||
isDefault: true,
|
||||
});
|
||||
s = await api.saveSettings({
|
||||
...s,
|
||||
vendorName: s.vendorName || "Self-test Vendor",
|
||||
vendorAddress: s.vendorAddress || "1 Test Road, Bengaluru - 560001",
|
||||
vendorStateCode: s.vendorStateCode || "29",
|
||||
defaultBankId: saved.id,
|
||||
onboarded: true,
|
||||
});
|
||||
banks = await api.listBanks();
|
||||
}
|
||||
const found = banks.find((b) => b.id === s.defaultBankId) ?? banks[0];
|
||||
if (!found) throw new Error("the fresh database has no bank account");
|
||||
if (!found) throw new Error("no bank account after provisioning");
|
||||
bank = found;
|
||||
return `${s.vendorName}, ${s.gstRegistration}, state ${s.vendorStateCode}, bank "${bank.label}"`;
|
||||
});
|
||||
|
||||
@@ -166,7 +166,7 @@ export function fixtureLogo(kind: LogoKind): LogoInput | null {
|
||||
if (!hit) {
|
||||
if (kind === "real") {
|
||||
// 2172 x 724: the untrimmed Test Vendor logo, a 3:1 aspect.
|
||||
const png = readFileSync(path.resolve(process.cwd(), "public/brand/sample-wordmark-logo.png"));
|
||||
const png = readFileSync(path.resolve(process.cwd(), "src/pdf/testing/fixtures/sample-wordmark-logo.png"));
|
||||
const [width, height] = [png.readUInt32BE(16), png.readUInt32BE(20)];
|
||||
hit = { dataUri: dataUri(png), width, height, aspect: width / height, density: 0.21, kind: "wordmark", includesName: true };
|
||||
} else if (kind === "knockout") {
|
||||
|
||||
@@ -440,7 +440,7 @@ export default function AppSettings({
|
||||
</section>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<DataBackupPanel />
|
||||
<DataBackupPanel settings={settings} onRerunSetup={onSaved} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<ErpnextSettingsPanel active={tab === SETTINGS_TABS.findIndex((t) => t.id === "integrations")} />
|
||||
|
||||
@@ -46,6 +46,8 @@ export default function Onboarding({ initial, onDone }: OnboardingProps) {
|
||||
const [bank, setBank] = useState<BankAccount>(bankers());
|
||||
const [prefix, setPrefix] = useState(`INV/${new Date().getFullYear()}-`);
|
||||
const [padding, setPadding] = useState(3);
|
||||
// The series that is active when the wizard opens. A re-run keeps it (and its counter) unless changed.
|
||||
const [loadedSeries, setLoadedSeries] = useState<{ prefix: string; padding: number } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
@@ -58,6 +60,7 @@ export default function Onboarding({ initial, onDone }: OnboardingProps) {
|
||||
if (series) {
|
||||
setPrefix(series.prefix);
|
||||
setPadding(series.padding);
|
||||
setLoadedSeries({ prefix: series.prefix, padding: series.padding });
|
||||
}
|
||||
} catch {
|
||||
/* first run – defaults are fine */
|
||||
@@ -89,7 +92,7 @@ export default function Onboarding({ initial, onDone }: OnboardingProps) {
|
||||
phoneError(form.vendorPhone) ||
|
||||
gstinProblem,
|
||||
),
|
||||
[form.vendorPan, form.vendorEmail, form.vendorPhone, gstinProblem],
|
||||
[form.vendorName, form.vendorStateCode, form.vendorPan, form.vendorEmail, form.vendorPhone, gstinProblem],
|
||||
);
|
||||
|
||||
const finish = async () => {
|
||||
@@ -97,10 +100,13 @@ export default function Onboarding({ initial, onDone }: OnboardingProps) {
|
||||
setError(null);
|
||||
try {
|
||||
const savedBank = await api.saveBank({ ...bank, isDefault: true });
|
||||
await api.startNewSeries(prefix.trim() || `INV/${new Date().getFullYear()}-`, padding);
|
||||
const wantedPrefix = prefix.trim() || `INV/${new Date().getFullYear()}-`;
|
||||
if (!loadedSeries || loadedSeries.prefix !== wantedPrefix || loadedSeries.padding !== padding) {
|
||||
await api.startNewSeries(wantedPrefix, padding);
|
||||
}
|
||||
const next: Settings = {
|
||||
...form,
|
||||
vendorName: form.vendorName.trim() || "Vendor",
|
||||
vendorName: form.vendorName.trim(),
|
||||
defaultBankId: savedBank.id,
|
||||
onboarded: true,
|
||||
};
|
||||
@@ -120,8 +126,7 @@ export default function Onboarding({ initial, onDone }: OnboardingProps) {
|
||||
<Grid narrow>
|
||||
<Column sm={4} md={8} lg={12}>
|
||||
<p className="voiced-muted" style={{ marginBottom: "1rem" }}>
|
||||
These details are printed on every invoice. They are pre-filled from your existing invoice —
|
||||
adjust anything that has changed.
|
||||
These details are printed on every invoice. You can change them later in Settings.
|
||||
</p>
|
||||
</Column>
|
||||
<Column sm={4} md={8} lg={12}>
|
||||
@@ -207,6 +212,7 @@ export default function Onboarding({ initial, onDone }: OnboardingProps) {
|
||||
value={form.vendorStateCode}
|
||||
onChange={(e) => set("vendorStateCode", e.target.value)}
|
||||
>
|
||||
<SelectItem value="" text="Choose your state" disabled hidden />
|
||||
{INDIAN_STATES.map((s) => (
|
||||
<SelectItem key={s.code} value={s.code} text={`${s.code} — ${s.name}`} />
|
||||
))}
|
||||
@@ -392,7 +398,7 @@ export default function Onboarding({ initial, onDone }: OnboardingProps) {
|
||||
{saving ? "Saving…" : "Finish setup"}
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={() => setStep((s) => s + 1)}>Next</Button>
|
||||
<Button disabled={step === 0 && businessInvalid} onClick={() => setStep((s) => s + 1)}>Next</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user