Add invoice drafts, read-only issued state and keyboard shortcuts
Views stay mounted so switching tabs no longer loses a half-typed invoice. The form autosaves a draft after 1 s, drafts can be resumed or deleted, and after issuing the form is read-only with Export again, Duplicate and New invoice actions. A failed export keeps the issued invoice and offers a retry instead of issuing again. Adds Ctrl+N/S/Enter/L and a shortcut sheet; F5 and Ctrl+R are blocked in production builds.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export interface ShortcutHandlers {
|
||||
onNew?: () => void;
|
||||
onSaveDraft?: () => void;
|
||||
onIssue?: () => void;
|
||||
onAddLine?: () => void;
|
||||
onHelp?: () => void;
|
||||
}
|
||||
|
||||
const isTyping = (target: EventTarget | null) => {
|
||||
const el = target as HTMLElement | null;
|
||||
if (!el) return false;
|
||||
return el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT" || el.isContentEditable;
|
||||
};
|
||||
|
||||
/**
|
||||
* Global keyboard shortcuts. Ctrl combinations work everywhere; the bare `?` is ignored
|
||||
* while typing. F5 / Ctrl+R are blocked in production builds because a webview reload
|
||||
* would discard in-memory state.
|
||||
*/
|
||||
export function useShortcuts(handlers: ShortcutHandlers) {
|
||||
const ref = useRef(handlers);
|
||||
ref.current = handlers;
|
||||
|
||||
useEffect(() => {
|
||||
const onKey = (event: KeyboardEvent) => {
|
||||
const ctrl = event.ctrlKey || event.metaKey;
|
||||
const key = event.key.toLowerCase();
|
||||
|
||||
if (import.meta.env.PROD && (event.key === "F5" || (ctrl && key === "r"))) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
const run = (fn?: () => void) => {
|
||||
event.preventDefault();
|
||||
fn?.();
|
||||
};
|
||||
|
||||
if (ctrl && !event.shiftKey && !event.altKey) {
|
||||
if (key === "n") return run(ref.current.onNew);
|
||||
if (key === "s") return run(ref.current.onSaveDraft);
|
||||
if (key === "l") return run(ref.current.onAddLine);
|
||||
if (key === "enter") return run(ref.current.onIssue);
|
||||
}
|
||||
if (!ctrl && !event.altKey && event.key === "?" && !isTyping(event.target)) {
|
||||
run(ref.current.onHelp);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, []);
|
||||
}
|
||||
|
||||
export const SHORTCUT_LIST: { keys: string; action: string }[] = [
|
||||
{ keys: "Ctrl + N", action: "New invoice" },
|
||||
{ keys: "Ctrl + S", action: "Save draft" },
|
||||
{ keys: "Ctrl + Enter", action: "Issue and export PDF" },
|
||||
{ keys: "Ctrl + L", action: "Add line item" },
|
||||
{ keys: "?", action: "Show this list" },
|
||||
];
|
||||
Reference in New Issue
Block a user