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";
import { autoAnimate } from "~/directives/autoAnimate";
import {
ADDRESS_LAYOUT_LEFT,
ADDRESS_LAYOUT_RIGHT,
CURRENT_VERSION,
LocalStoreContext,
localStoreSchema,
migrateInfoLog,
migrateLocalState,
migrateState,
POSITION_TYPE_AGILE,
POSITION_TYPE_QUANTITY,
PrintType,
@ -727,9 +733,22 @@ const SettingsOverlay: Component = () => {
<AccordionContent>
<AccordionItemDivider>Corporate Design</AccordionItemDivider>
<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">
<TextInput
label="Logo"
labelMinWidth={fullWidthLabelWidth}
type="file"
class="file-input"
accept="image/png, image/jpeg, image/svg+xml"
@ -837,8 +856,6 @@ const SettingsOverlay: Component = () => {
return;
}
// TODO: Run migrations
const schema = z
.object({
state: storeSchema,
@ -847,6 +864,29 @@ const SettingsOverlay: Component = () => {
.collectErrors();
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);
} catch (e: any) {
const message = "Das Dokument hat kein gültiges Format.";

@ -7,11 +7,15 @@ export const createLocalStore = function <T extends Record<string, any>>(
prefix = "app",
serializer = (v: any) => JSON.stringify(v),
deserializer = (v: any) => JSON.parse(v),
migrate = undefined as
| ((getState: () => Record<string, any>) => boolean)
| undefined,
} = {}
) {
const [state, setState] = createStore(initState);
const [mounted, setMounted] = createSignal(false);
const localStorage = globalThis.localStorage;
const storePrefix = `${prefix}-`;
if (localStorage) {
let mounts = 0;
@ -20,8 +24,36 @@ export const createLocalStore = function <T extends Record<string, any>>(
const keys = Object.keys(state);
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) {
let storeKey = `${prefix}-${key}`;
let storeKey = `${storePrefix}${key}`;
let mountValue = localStorage.getItem(storeKey);
let initRun = true;
const [updatingCount, setUpdatingCount] = createSignal(0);

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

@ -49,6 +49,12 @@ export const createUiStore = () =>
export type UiStore = ReturnType<typeof createUiStore>;
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({
version: z.number(),
project: z.object({
@ -80,9 +86,15 @@ export const storeSchema = z.object({
});
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 = () =>
createStore_<StoreObject>({
version: 1,
version: CURRENT_VERSION,
project: {
orderNumber: "",
projectNumber: "",
@ -120,6 +132,7 @@ export const localStoreSchema = z.object({
paymentTerms: z.string().optional(),
iban: z.string(),
creditor: addressSchema,
addressLayout: z.literals(ADDRESS_LAYOUT_LEFT, ADDRESS_LAYOUT_RIGHT),
customAddress: addressSchema,
contact: z.object({
name: z.string(),
@ -140,15 +153,31 @@ export const localStoreSchema = z.object({
});
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 = () =>
createLocalStore_<LocalStoreObject>(
{
version: 1,
version: CURRENT_VERSION,
showWelcome: true,
vatNumber: "",
vatRate: 0.0,
paymentTerms: undefined,
creditor: createAddress(),
addressLayout: ADDRESS_LAYOUT_RIGHT,
customAddress: createAddress(),
contact: {
name: "",
@ -160,7 +189,24 @@ export const createLocalStore = () =>
useCustomAddress: false,
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>;

Loading…
Cancel
Save