- Settings gets an Integrations tab: connection fields with a write-only API secret, a CA PEM option, Test connection with versions and warnings, live mapping lists loaded after a test, naming mode, toggles and its own Save. - Pushing for a GST-registered vendor is refused until a connection test result is stored. The UI shows the reason and disables Send. - History shows an ERPNext status column from one bulk call, a row Send and Open in ERPNext, and a bulk Send selected with a draft-or-submit confirm step and per-row results that never abort the batch. The detail view gets an ERPNext panel and per-payment send. All of it is hidden or disabled when the integration is not configured. - Optional auto-push after issue runs in the background and never blocks or fails the issue. - Generic export: a deterministic voiced.invoice.v1 JSON (and CSV line items) for selected invoices, documented in docs/voiced-invoice-v1.md with a golden test. Money is integer paise plus a decimal string. The new screens have not been run in a webview yet. The ERPNext Payment Entry deduction fields still need a live check.
53 lines
1.6 KiB
TypeScript
53 lines
1.6 KiB
TypeScript
const KEY = "voiced.settingsTab";
|
|
|
|
export const SETTINGS_TABS = [
|
|
{ id: "business", label: "Business" },
|
|
{ id: "branding", label: "Branding" },
|
|
{ id: "bank", label: "Bank & payments" },
|
|
{ id: "defaults", label: "Invoice defaults" },
|
|
{ id: "numbering", label: "Numbering" },
|
|
{ id: "templates", label: "Templates & export" },
|
|
{ id: "fonts", label: "Fonts" },
|
|
{ id: "data", label: "Data" },
|
|
{ id: "integrations", label: "Integrations" },
|
|
] as const;
|
|
|
|
export type SettingsTabId = (typeof SETTINGS_TABS)[number]["id"];
|
|
|
|
/** Index of a stored tab id; 0 (Business) for anything unknown. */
|
|
export function parseSettingsTab(value: string | null | undefined): number {
|
|
const index = SETTINGS_TABS.findIndex((t) => t.id === value);
|
|
return index < 0 ? 0 : index;
|
|
}
|
|
|
|
/** A tab another screen asked for; used once by the next Settings mount, so it works without localStorage. */
|
|
let requested: SettingsTabId | null = null;
|
|
|
|
/** Makes the next opening of Settings land on `id` (for "Set up in Settings" links). */
|
|
export function requestSettingsTab(id: SettingsTabId): void {
|
|
requested = id;
|
|
}
|
|
|
|
/** The last Settings tab this viewer used. A per-viewer convenience only. */
|
|
export function loadSettingsTab(): number {
|
|
if (requested) {
|
|
const index = parseSettingsTab(requested);
|
|
requested = null;
|
|
return index;
|
|
}
|
|
try {
|
|
return parseSettingsTab(localStorage.getItem(KEY));
|
|
} catch {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
export function saveSettingsTab(index: number): void {
|
|
try {
|
|
const tab = SETTINGS_TABS[index];
|
|
if (tab) localStorage.setItem(KEY, tab.id);
|
|
} catch {
|
|
// Not remembering is harmless.
|
|
}
|
|
}
|