feat: implement addressLayout setting and optimize address layouts for print

closes issue #14
master
Katja Lutz 2 years ago
parent c9328257db
commit 24e82ace96

@ -29,8 +29,14 @@ import {
} from "../Form"; } from "../Form";
import { autoAnimate } from "~/directives/autoAnimate"; import { autoAnimate } from "~/directives/autoAnimate";
import { import {
ADDRESS_LAYOUT_LEFT,
ADDRESS_LAYOUT_RIGHT,
CURRENT_VERSION,
LocalStoreContext, LocalStoreContext,
localStoreSchema, localStoreSchema,
migrateInfoLog,
migrateLocalState,
migrateState,
POSITION_TYPE_AGILE, POSITION_TYPE_AGILE,
POSITION_TYPE_QUANTITY, POSITION_TYPE_QUANTITY,
PrintType, PrintType,
@ -727,9 +733,22 @@ const SettingsOverlay: Component = () => {
<AccordionContent> <AccordionContent>
<AccordionItemDivider>Corporate Design</AccordionItemDivider> <AccordionItemDivider>Corporate Design</AccordionItemDivider>
<AccordionItemGrid> <AccordionItemGrid>
<div class="col-span-2">
<Select
label="Adress-Layout"
labelMinWidth={fullWidthLabelWidth}
options={[
[ADDRESS_LAYOUT_LEFT, "Adresse links"],
[ADDRESS_LAYOUT_RIGHT, "Adresse rechts"],
]}
value={localState.addressLayout}
onChange={(v) => setLocalState("addressLayout", v)}
/>
</div>
<div class="col-span-2"> <div class="col-span-2">
<TextInput <TextInput
label="Logo" label="Logo"
labelMinWidth={fullWidthLabelWidth}
type="file" type="file"
class="file-input" class="file-input"
accept="image/png, image/jpeg, image/svg+xml" accept="image/png, image/jpeg, image/svg+xml"
@ -837,8 +856,6 @@ const SettingsOverlay: Component = () => {
return; return;
} }
// TODO: Run migrations
const schema = z const schema = z
.object({ .object({
state: storeSchema, state: storeSchema,
@ -847,6 +864,29 @@ const SettingsOverlay: Component = () => {
.collectErrors(); .collectErrors();
try { try {
let migrated = false;
const localStateVersion =
contentObject?.localState?.version;
const stateVersion = contentObject?.state?.version;
if (localStateVersion !== CURRENT_VERSION) {
migrateLocalState(contentObject?.localState);
migrated = true;
}
if (stateVersion !== CURRENT_VERSION) {
migrateState(contentObject?.state);
migrated = true;
}
if (migrated) {
migrateInfoLog(
localStateVersion < stateVersion
? localStateVersion
: stateVersion,
CURRENT_VERSION
);
}
schema.parse(contentObject); schema.parse(contentObject);
} catch (e: any) { } catch (e: any) {
const message = "Das Dokument hat kein gültiges Format."; const message = "Das Dokument hat kein gültiges Format.";

@ -7,11 +7,15 @@ export const createLocalStore = function <T extends Record<string, any>>(
prefix = "app", prefix = "app",
serializer = (v: any) => JSON.stringify(v), serializer = (v: any) => JSON.stringify(v),
deserializer = (v: any) => JSON.parse(v), deserializer = (v: any) => JSON.parse(v),
migrate = undefined as
| ((getState: () => Record<string, any>) => boolean)
| undefined,
} = {} } = {}
) { ) {
const [state, setState] = createStore(initState); const [state, setState] = createStore(initState);
const [mounted, setMounted] = createSignal(false); const [mounted, setMounted] = createSignal(false);
const localStorage = globalThis.localStorage; const localStorage = globalThis.localStorage;
const storePrefix = `${prefix}-`;
if (localStorage) { if (localStorage) {
let mounts = 0; let mounts = 0;
@ -20,8 +24,36 @@ export const createLocalStore = function <T extends Record<string, any>>(
const keys = Object.keys(state); const keys = Object.keys(state);
const changedBeforeMount = {} as Record<string, any>; const changedBeforeMount = {} as Record<string, any>;
if (migrate) {
let migratedState: Record<string, any> | undefined = undefined;
const getMigrateState = () => {
if (migratedState) return migratedState;
migratedState = {};
for (const key of Object.keys(localStorage)) {
if (!key.startsWith(storePrefix)) {
continue;
}
migratedState[key.slice(storePrefix.length)] = deserializer(
localStorage.getItem(key)
);
}
return migratedState;
};
const migrated = migrate(getMigrateState);
if (migrated && migratedState != undefined) {
for (const [key, value] of Object.entries(migratedState)) {
localStorage.setItem(`${storePrefix}${key}`, serializer(value));
}
}
migratedState = undefined;
}
for (const key of keys) { for (const key of keys) {
let storeKey = `${prefix}-${key}`; let storeKey = `${storePrefix}${key}`;
let mountValue = localStorage.getItem(storeKey); let mountValue = localStorage.getItem(storeKey);
let initRun = true; let initRun = true;
const [updatingCount, setUpdatingCount] = createSignal(0); const [updatingCount, setUpdatingCount] = createSignal(0);

@ -22,6 +22,8 @@ import Positions, {
} from "~/components/Positions"; } from "~/components/Positions";
import SettingsOverlay from "~/components/Settings/Overlay"; import SettingsOverlay from "~/components/Settings/Overlay";
import { import {
ADDRESS_LAYOUT_LEFT,
ADDRESS_LAYOUT_RIGHT,
createLocalStore, createLocalStore,
createStore, createStore,
createUiStore, createUiStore,
@ -157,8 +159,15 @@ export default function Home() {
<Meta property="og:url" content={getHost()} /> <Meta property="og:url" content={getHost()} />
<Meta name="twitter:card" content="summary_large_image" /> <Meta name="twitter:card" content="summary_large_image" />
<Meta name="twitter:site" content="@katy_wings" /> <Meta name="twitter:site" content="@katy_wings" />
<Show when={localState.logo}> <div
<div class="flex justify-end items-center h-20 mb-5"> classList={{
"flex justify-start items-center print:mb-7 print:h-28": true,
"h-20 mb-7": !!localState.logo,
"justify-start": localState.addressLayout == ADDRESS_LAYOUT_RIGHT,
"justify-end": localState.addressLayout == ADDRESS_LAYOUT_LEFT,
}}
>
<Show when={localState.logo}>
<img <img
classList={{ classList={{
"max-h-full max-w-[50%] w-auto": true, "max-h-full max-w-[50%] w-auto": true,
@ -169,75 +178,88 @@ export default function Home() {
height={localState.logo?.height} height={localState.logo?.height}
src={localState.logo?.url} src={localState.logo?.url}
/> />
</div> </Show>
</Show>
<div class="text-xs mb-2">
{address().name
? [address().name, getLine1(address()), getLine2(address())]
.filter((x) => x != "")
.join(" · ")
: ""}
</div> </div>
<div class="grid grid-cols-2 gap-x-[30%] mb-10"> <div class="print:min-h-[12rem] mb-10">
<div class="leading-snug"> <div class="grid grid-cols-2 gap-x-[15%]">
<div>{customerAddress().name}</div> <div
<div>{getLine1(customerAddress())}</div> classList={{
<div>{getLine2(customerAddress())}</div> "order-last": localState.addressLayout == ADDRESS_LAYOUT_RIGHT,
</div> }}
<div class="grid grid-cols-2 gap-x-4 text-sm">
<RightItem
class="font-bold"
label="Projekt Nr."
value={state.project.projectNumber}
/>
<RightItem
class="font-bold"
label="Bestellungs Nr."
value={state.project.orderNumber}
/>
<RightItem
value={getDisplayDateFromUnix(state.project.date)}
label="Datum"
/>
<RightItem
label="Lieferungs Nr."
value={state.project.deliveryNumber}
/>
<RightItem
label="Lieferdatum"
value={
state.project.deliveryDate != null &&
getDisplayDateFromUnix(state.project.deliveryDate)
}
/>
<RightItem
label="Kunden Nr."
value={state.customer.customerNumber}
/>
<RightItem label="Ihre MwST-Nr." value={state.customer.vatNumber} />
<Show
when={
localState.contact.name ||
localState.contact.phone ||
localState.contact.email
}
> >
<hr class="col-span-2 my-2" /> <div class="text-sm mb-3">
<RightItem {address().name
value={localState.contact.name} ? [address().name, getLine1(address()), getLine2(address())]
label="Ansprechpartner" .filter((x) => x != "")
/> .join(" · ")
<RightItem value={localState.contact.phone} label="Telefon" /> : ""}
<RightItem </div>
value={localState.contact.email} <div class="text-lg leading-snug">
label="E-Mail Adresse" <div>{customerAddress().name}</div>
/> <div>{getLine1(customerAddress())}</div>
</Show> <div>{getLine2(customerAddress())}</div>
</div>
</div>
<div>
<div class="grid grid-cols-2 gap-x-4 text-sm">
<RightItem
class="font-bold"
label="Projekt Nr."
value={state.project.projectNumber}
/>
<RightItem
class="font-bold"
label="Bestellungs Nr."
value={state.project.orderNumber}
/>
<RightItem
value={getDisplayDateFromUnix(state.project.date)}
label="Datum"
/>
<RightItem
label="Lieferungs Nr."
value={state.project.deliveryNumber}
/>
<RightItem
label="Lieferdatum"
value={
state.project.deliveryDate != null &&
getDisplayDateFromUnix(state.project.deliveryDate)
}
/>
<RightItem
label="Kunden Nr."
value={state.customer.customerNumber}
/>
<RightItem
label="Ihre MwST-Nr."
value={state.customer.vatNumber}
/>
<Show
when={
localState.contact.name ||
localState.contact.phone ||
localState.contact.email
}
>
<hr class="col-span-2 my-2" />
<RightItem
value={localState.contact.name}
label="Ansprechpartner"
/>
<RightItem value={localState.contact.phone} label="Telefon" />
<RightItem
value={localState.contact.email}
label="E-Mail Adresse"
/>
</Show>
<Show when={localState.vatNumber}> <Show when={localState.vatNumber}>
<div class="col-span-2 h-4"></div> <div class="col-span-2 h-4"></div>
<RightItem label="MwST-Nr." value={localState.vatNumber} /> <RightItem label="MwST-Nr." value={localState.vatNumber} />
</Show> </Show>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -286,7 +308,7 @@ export default function Home() {
<Style type="text/css">{` <Style type="text/css">{`
@page { @page {
size: A4 portrait; size: A4 portrait;
margin: 11mm ${state.fullWidthInvoice ? 0 : 11}mm 11mm ${ margin: 25mm ${state.fullWidthInvoice ? 0 : 11}mm 11mm ${
state.fullWidthInvoice ? 0 : 11 state.fullWidthInvoice ? 0 : 11
}mm; }mm;
} }
@ -297,7 +319,7 @@ export default function Home() {
@media print { @media print {
html { html {
font-size: 10px; font-size: 11px;
} }
} }
`}</Style> `}</Style>

@ -49,6 +49,12 @@ export const createUiStore = () =>
export type UiStore = ReturnType<typeof createUiStore>; export type UiStore = ReturnType<typeof createUiStore>;
export const UiStoreContext = createContext<UiStore>(); export const UiStoreContext = createContext<UiStore>();
export const ADDRESS_LAYOUT_LEFT = "LEFT";
export const ADDRESS_LAYOUT_RIGHT = "RIGHT";
export type AddressLayout =
| typeof ADDRESS_LAYOUT_LEFT
| typeof ADDRESS_LAYOUT_RIGHT;
export const storeSchema = z.object({ export const storeSchema = z.object({
version: z.number(), version: z.number(),
project: z.object({ project: z.object({
@ -80,9 +86,15 @@ export const storeSchema = z.object({
}); });
export type StoreObject = Infer<typeof storeSchema>; export type StoreObject = Infer<typeof storeSchema>;
export const CURRENT_VERSION = 1.3;
export const migrateState = (state: Record<string, any>) => {
state.version = CURRENT_VERSION;
};
export const createStore = () => export const createStore = () =>
createStore_<StoreObject>({ createStore_<StoreObject>({
version: 1, version: CURRENT_VERSION,
project: { project: {
orderNumber: "", orderNumber: "",
projectNumber: "", projectNumber: "",
@ -120,6 +132,7 @@ export const localStoreSchema = z.object({
paymentTerms: z.string().optional(), paymentTerms: z.string().optional(),
iban: z.string(), iban: z.string(),
creditor: addressSchema, creditor: addressSchema,
addressLayout: z.literals(ADDRESS_LAYOUT_LEFT, ADDRESS_LAYOUT_RIGHT),
customAddress: addressSchema, customAddress: addressSchema,
contact: z.object({ contact: z.object({
name: z.string(), name: z.string(),
@ -140,15 +153,31 @@ export const localStoreSchema = z.object({
}); });
export type LocalStoreObject = Infer<typeof localStoreSchema>; export type LocalStoreObject = Infer<typeof localStoreSchema>;
export const migrateLocalState = (state: Record<string, any>) => {
if (state.version === 1) {
state.addressLayout = ADDRESS_LAYOUT_LEFT;
state.version = 1.3;
}
state.version = CURRENT_VERSION;
};
export const migrateInfoLog = (oldVersion: number, newVersion: number) =>
console.info(
`Migrated document schema from version ${oldVersion} to ${newVersion}`
);
export const createLocalStore = () => export const createLocalStore = () =>
createLocalStore_<LocalStoreObject>( createLocalStore_<LocalStoreObject>(
{ {
version: 1, version: CURRENT_VERSION,
showWelcome: true, showWelcome: true,
vatNumber: "", vatNumber: "",
vatRate: 0.0, vatRate: 0.0,
paymentTerms: undefined, paymentTerms: undefined,
creditor: createAddress(), creditor: createAddress(),
addressLayout: ADDRESS_LAYOUT_RIGHT,
customAddress: createAddress(), customAddress: createAddress(),
contact: { contact: {
name: "", name: "",
@ -160,7 +189,24 @@ export const createLocalStore = () =>
useCustomAddress: false, useCustomAddress: false,
iban: "", iban: "",
}, },
{ prefix: "invoice-app" } {
prefix: "invoice-app",
migrate: (getState) => {
const version = JSON.parse(
localStorage.getItem("invoice-app-version") || "-1"
);
if (version == -1) return false;
if (version !== CURRENT_VERSION) {
migrateLocalState(getState());
migrateInfoLog(version, CURRENT_VERSION);
return true;
}
return false;
},
}
); );
export type LocalStore = ReturnType<typeof createLocalStore>; export type LocalStore = ReturnType<typeof createLocalStore>;

Loading…
Cancel
Save