Linea (the vendor's original format.pdf design), Monogram and Serenity are rebuilt from the reference PDFs as one parametric family on the engine, with the user's logo in the measured slot, a home for every GST field and A4/Letter support. Adds static Montserrat, Open Sans and Poppins fonts and a generated Ms Madi outline for Serenity's vertical word. Text-only logo placeholders (Monolith, Tangerine) use a doc-derived allowance box. A Carbon TemplatePicker with committed thumbnails opens from the page setup controls.
131 lines
4.6 KiB
TypeScript
131 lines
4.6 KiB
TypeScript
import { useState } from "react";
|
|
import { Button, ContentSwitcher, InlineLoading, Select, SelectItem, Switch, Toggle } from "@carbon/react";
|
|
import { Catalog } from "@carbon/icons-react";
|
|
import { api } from "../lib/api";
|
|
import type { Settings } from "../lib/types";
|
|
import type { MarginPreset, PageId } from "../pdf/engine/geometry";
|
|
import { parsePrefs, prefsToJson, type RenderPrefsV1 } from "../pdf/model/prefs";
|
|
import { catalogEntry } from "../pdf/templates/catalog";
|
|
import TemplatePicker from "./TemplatePicker";
|
|
|
|
const PAGES: { id: PageId; label: string }[] = [
|
|
{ id: "A4", label: "A4" },
|
|
{ id: "LETTER", label: "US Letter" },
|
|
];
|
|
|
|
const MARGINS: { id: MarginPreset; label: string }[] = [
|
|
{ id: "template", label: "Template default" },
|
|
{ id: "narrow", label: "Narrow (0.5″)" },
|
|
{ id: "normal", label: "Normal (0.75″)" },
|
|
];
|
|
|
|
interface Props {
|
|
prefs: RenderPrefsV1;
|
|
onChange: (prefs: RenderPrefsV1) => void;
|
|
/** Live settings; `renderPrefs` is the saved default. */
|
|
settings: Settings;
|
|
/** Called after the default was saved so the app reloads its settings. */
|
|
onSaved: () => void;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
/** Page size and margins for the invoice being edited, plus "Save as default" for new invoices. */
|
|
export default function PageSetupControls({ prefs, onChange, settings, onSaved, disabled }: Props) {
|
|
const [saving, setSaving] = useState(false);
|
|
const [pickerOpen, setPickerOpen] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const saved = prefsToJson(parsePrefs(settings.renderPrefs));
|
|
const isDefault = prefsToJson(prefs) === saved;
|
|
const template = catalogEntry(prefs.templateId);
|
|
|
|
const saveDefault = async () => {
|
|
setSaving(true);
|
|
setError(null);
|
|
try {
|
|
await api.saveSettings({ ...settings, renderPrefs: prefsToJson(prefs) });
|
|
onSaved();
|
|
} catch (e) {
|
|
setError(String(e));
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="voiced-page-setup">
|
|
<div className="voiced-page-setup__row">
|
|
<div className="voiced-page-setup__field">
|
|
<span className="cds--label">Template</span>
|
|
<Button kind="tertiary" size="md" renderIcon={Catalog} disabled={disabled} onClick={() => setPickerOpen(true)}>
|
|
{template.name}
|
|
</Button>
|
|
</div>
|
|
<div className="voiced-page-setup__field">
|
|
<span className="cds--label">Page size</span>
|
|
<ContentSwitcher
|
|
size="md"
|
|
selectedIndex={Math.max(0, PAGES.findIndex((p) => p.id === prefs.pageSize))}
|
|
onChange={(e) => !disabled && onChange({ ...prefs, pageSize: PAGES[e.index ?? 0].id })}
|
|
>
|
|
{PAGES.map((p) => (
|
|
<Switch key={p.id} name={p.id} text={p.label} disabled={disabled} />
|
|
))}
|
|
</ContentSwitcher>
|
|
</div>
|
|
<div className="voiced-page-setup__field">
|
|
<Select
|
|
id="page-margins"
|
|
labelText="Margins"
|
|
size="md"
|
|
disabled={disabled}
|
|
value={prefs.margins}
|
|
onChange={(e) => onChange({ ...prefs, margins: e.target.value as MarginPreset })}
|
|
>
|
|
{MARGINS.map((m) => (
|
|
<SelectItem key={m.id} value={m.id} text={m.label} />
|
|
))}
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
{template.logoOptIn ? (
|
|
<div className="voiced-page-setup__row voiced-page-setup__extra">
|
|
<Toggle
|
|
id="template-logo-show"
|
|
size="sm"
|
|
labelText="Logo"
|
|
labelA="Hidden"
|
|
labelB="Shown above the title"
|
|
toggled={prefs.logo?.show === true}
|
|
disabled={disabled}
|
|
onToggle={(on) => onChange({ ...prefs, logo: { ...prefs.logo, show: on } })}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
<TemplatePicker
|
|
open={pickerOpen}
|
|
onClose={() => setPickerOpen(false)}
|
|
selectedId={template.id}
|
|
onSelect={(id) => onChange({ ...prefs, templateId: id })}
|
|
isDefault={isDefault}
|
|
saving={saving}
|
|
error={error}
|
|
onSaveDefault={() => void saveDefault()}
|
|
disabled={disabled}
|
|
/>
|
|
<div className="voiced-page-setup__footer">
|
|
{saving ? (
|
|
<InlineLoading status="active" description="Saving…" />
|
|
) : (
|
|
<Button kind="tertiary" size="sm" disabled={isDefault} onClick={() => void saveDefault()}>
|
|
{isDefault ? "Saved as default" : "Save as default"}
|
|
</Button>
|
|
)}
|
|
<span className="voiced-muted voiced-page-setup__note">
|
|
{error ?? "This choice is frozen into each invoice when it is issued."}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|