You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
rappli/src/stores.ts

214 lines
5.8 KiB
TypeScript

import { createContext } from "solid-js";
import { createStore as createStore_ } from "solid-js/store";
import { addressSchema, createAddress } from "./components/Address";
import { createLocalStore as createLocalStore_ } from "./localStore";
import { getUnixTime } from "date-fns";
import z, { Infer } from "myzod";
export const POSITION_TYPE_QUANTITY = "QUANTITY";
export const POSITION_TYPE_AGILE = "AGILE";
export const positionSchema = z.object({
id: z.number(),
name: z.string(),
enabled: z.boolean(),
type: z.literals(POSITION_TYPE_QUANTITY, POSITION_TYPE_AGILE),
agilePointsMin: z.number().optional(),
agilePointsMax: z.number().optional(),
agileRiskFactor: z.number().optional(),
number: z.string().optional(),
description: z.string().optional(),
quantity: z.number(),
fixedDiscountPrice: z.number().optional(),
itemPrice: z.number().optional(),
});
export type Position = Infer<typeof positionSchema>;
export const PRINT_TYPE_OFFER = "OFFER";
export const PRINT_TYPE_CONFIRMATION = "CONFIRMATION";
export const PRINT_TYPE_INVOICE = "INVOICE";
export type PrintType =
| typeof PRINT_TYPE_OFFER
| typeof PRINT_TYPE_CONFIRMATION
| typeof PRINT_TYPE_INVOICE;
export const printTypeTitles = {
[PRINT_TYPE_OFFER]: "Offerte",
[PRINT_TYPE_CONFIRMATION]: "Bestätigung",
[PRINT_TYPE_INVOICE]: "Rechnung",
};
export const createUiStore = () =>
createStore_({
lastSaved: 0,
printType: PRINT_TYPE_INVOICE as PrintType,
selectedPosition: undefined as undefined | number,
});
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({
orderNumber: z.string(),
projectNumber: z.string(),
deliveryNumber: z.string(),
deliveryDate: z.number().optional(),
date: z.number(),
preface: z.string().optional(),
conclusion: z.string().optional(),
}),
invoice: z.object({
reference: z.string(),
message: z.string(),
}),
fullWidthInvoice: z.boolean(),
useCustomerAlternativeAddress: z.boolean(),
customer: z.object({
customerNumber: z.string(),
vatNumber: z.string(),
alternativeAddress: addressSchema,
debtorAddress: addressSchema,
}),
defaultItemPrice: z.number(),
agileRiskFactor: z.number(),
agileHoursPerStoryPoint: z.number(),
defaultPositionType: z.literals(POSITION_TYPE_QUANTITY, POSITION_TYPE_AGILE),
positions: z.array(positionSchema),
});
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: CURRENT_VERSION,
project: {
orderNumber: "",
projectNumber: "",
deliveryNumber: "",
date: getUnixTime(new Date()),
},
invoice: {
reference: "",
message: "",
},
fullWidthInvoice: true,
useCustomerAlternativeAddress: false,
customer: {
customerNumber: "",
vatNumber: "",
alternativeAddress: createAddress(),
debtorAddress: createAddress(),
},
defaultPositionType: POSITION_TYPE_QUANTITY,
defaultItemPrice: 0,
agileRiskFactor: 0.7,
agileHoursPerStoryPoint: 4,
positions: [] as Position[],
});
export type Store = ReturnType<typeof createStore>;
export const StoreContext = createContext<Store>();
export const localStoreSchema = z.object({
// TODO: Move this to a cookie (advantage: ssr)
showWelcome: z.boolean(),
version: z.number(),
vatNumber: z.string(),
vatRate: z.number(),
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(),
phone: z.string(),
email: z.string(),
}),
showLufraiWatermark: z.boolean(),
// TODO: Signature Image
logo: z
.object({
type: z.string(),
url: z.string(),
width: z.number(),
height: z.number(),
})
.optional(),
useCustomAddress: z.boolean(),
});
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: CURRENT_VERSION,
showWelcome: true,
vatNumber: "",
vatRate: 0.0,
paymentTerms: undefined,
creditor: createAddress(),
addressLayout: ADDRESS_LAYOUT_RIGHT,
customAddress: createAddress(),
contact: {
name: "",
phone: "",
email: "",
},
logo: undefined,
showLufraiWatermark: true,
useCustomAddress: false,
iban: "",
},
{
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 const LocalStoreContext = createContext<LocalStore>();