Compare commits

...

10 Commits

@ -2,6 +2,23 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [1.4.0](https://git.lufrai.com/rappli/rappli/compare/v1.3.0...v1.4.0) (2022-09-15)
### Features
* implement custom page title setting ([c35a017](https://git.lufrai.com/rappli/rappli/commit/c35a017d910eec83b6980497e93fcc833c9af6a1)), closes [#21](https://git.lufrai.com/rappli/rappli/issues/21)
* implement IBAN validation ([73b9024](https://git.lufrai.com/rappli/rappli/commit/73b9024b055e562ae474a457a811572f5940ed64)), closes [#6](https://git.lufrai.com/rappli/rappli/issues/6)
* implement letter print type ([f8d0b9e](https://git.lufrai.com/rappli/rappli/commit/f8d0b9e6e8c84113dba622b1a6c02a845d0d3714))
* uppercase zip and iban input labels ([ab73b21](https://git.lufrai.com/rappli/rappli/commit/ab73b219714bca6221ffcbca9725983cfb7f2fc3))
### Bug Fixes
* dont apply margin-bottom to prose br ([00cdbd8](https://git.lufrai.com/rappli/rappli/commit/00cdbd853d202081ffff7e6e1eaf265396fba039))
* properly clear textarea values after file load ([decfdf8](https://git.lufrai.com/rappli/rappli/commit/decfdf838d8ca5807706a09f3ef32c59e8932ef7))
* set proper swiss qr version as expected by the guidelines ([5548b47](https://git.lufrai.com/rappli/rappli/commit/5548b4725302771c5378da2162351689c97927c0)), closes [#23](https://git.lufrai.com/rappli/rappli/issues/23)
## [1.3.0](https://git.lufrai.com/rappli/rappli/compare/v1.2.0...v1.3.0) (2022-07-28) ## [1.3.0](https://git.lufrai.com/rappli/rappli/compare/v1.2.0...v1.3.0) (2022-07-28)

@ -25,6 +25,12 @@ npm run dev
npm run dev -- --open npm run dev -- --open
``` ```
### Guidelines & Validation of Swiss Qr Codes
- Guidelines: https://www.paymentstandards.ch/dam/downloads/ig-qr-bill-de.pdf
- Swico QR Validator: https://www.swiss-qr-invoice.org/validator/?lang=de
- Six QR Validator: https://validation.iso-payments.ch/gp/qrrechnung/validation
## Building ## Building
Solid apps are built with _adapters_, which optimise your project for deployment to different environments. Solid apps are built with _adapters_, which optimise your project for deployment to different environments.

17
package-lock.json generated

@ -1,12 +1,12 @@
{ {
"name": "rappli", "name": "rappli",
"version": "1.3.0", "version": "1.4.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "rappli", "name": "rappli",
"version": "1.3.0", "version": "1.4.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"solid-start": "v0.1.0-alpha.89", "solid-start": "v0.1.0-alpha.89",
@ -30,6 +30,7 @@
"daisyui": "^2.20.0", "daisyui": "^2.20.0",
"date-fns": "^2.29.1", "date-fns": "^2.29.1",
"froebel": "^0.18.0", "froebel": "^0.18.0",
"ibantools": "^4.1.6",
"myzod": "^1.8.7", "myzod": "^1.8.7",
"nanoid": "^4.0.0", "nanoid": "^4.0.0",
"node-iso11649": "^2.1.2", "node-iso11649": "^2.1.2",
@ -4249,6 +4250,12 @@
"node": ">=10.17.0" "node": ">=10.17.0"
} }
}, },
"node_modules/ibantools": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/ibantools/-/ibantools-4.1.6.tgz",
"integrity": "sha512-BpIqMJj6tgFbx1YmH7tjstiqf8VgLkGL2d3K7XSOuGxbdy6gv6ovKjC/Yqgrh3OEeqeSzx8fVu5P96Q1bWg2bg==",
"dev": true
},
"node_modules/indent-string": { "node_modules/indent-string": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
@ -11180,6 +11187,12 @@
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==" "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="
}, },
"ibantools": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/ibantools/-/ibantools-4.1.6.tgz",
"integrity": "sha512-BpIqMJj6tgFbx1YmH7tjstiqf8VgLkGL2d3K7XSOuGxbdy6gv6ovKjC/Yqgrh3OEeqeSzx8fVu5P96Q1bWg2bg==",
"dev": true
},
"indent-string": { "indent-string": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",

@ -1,6 +1,6 @@
{ {
"name": "rappli", "name": "rappli",
"version": "1.3.0", "version": "1.4.0",
"bin": "./bin/rappli.js", "bin": "./bin/rappli.js",
"scripts": { "scripts": {
"dev": "solid-start dev", "dev": "solid-start dev",
@ -32,6 +32,7 @@
"daisyui": "^2.20.0", "daisyui": "^2.20.0",
"date-fns": "^2.29.1", "date-fns": "^2.29.1",
"froebel": "^0.18.0", "froebel": "^0.18.0",
"ibantools": "^4.1.6",
"myzod": "^1.8.7", "myzod": "^1.8.7",
"nanoid": "^4.0.0", "nanoid": "^4.0.0",
"node-iso11649": "^2.1.2", "node-iso11649": "^2.1.2",

@ -28,6 +28,7 @@ export const TextInput: Component<
suffix?: string | JSX.Element; suffix?: string | JSX.Element;
size?: string; size?: string;
vertical?: boolean; vertical?: boolean;
invalidate?: (v: any) => string | boolean;
} & JSX.InputHTMLAttributes<HTMLInputElement> } & JSX.InputHTMLAttributes<HTMLInputElement>
> = (p) => { > = (p) => {
p = mergeProps( p = mergeProps(
@ -48,6 +49,7 @@ export const TextInput: Component<
"suffix", "suffix",
"vertical", "vertical",
"labelMinWidth", "labelMinWidth",
"invalidate",
]); ]);
const sizes: Record<string, string> = { const sizes: Record<string, string> = {
xs: "input-xs", xs: "input-xs",
@ -55,7 +57,10 @@ export const TextInput: Component<
lg: "input-lg", lg: "input-lg",
}; };
const [validate, vState] = validateInput({ value: () => rest.value }); const [validate, vState] = validateInput({
invalidate: props.invalidate,
value: () => rest.value,
});
return ( return (
<div class="shrink form-control"> <div class="shrink form-control">
@ -167,14 +172,13 @@ export const TextArea: Component<
<textarea <textarea
autocomplete="off" autocomplete="off"
ref={textareaEl} ref={textareaEl}
value={props.value || ""}
classList={{ classList={{
"textarea h-auto py-2 textarea-bordered leading-normal": true, "textarea h-auto py-2 textarea-bordered leading-normal": true,
"min-h-[150px]": autosizeEnabled(), "min-h-[150px]": autosizeEnabled(),
}} }}
{...rest} {...rest}
> />
{props.value || ""}
</textarea>
</label> </label>
</div> </div>
); );

@ -17,6 +17,7 @@ import z from "myzod";
import Big from "big.js"; import Big from "big.js";
import { generate } from "node-iso11649"; import { generate } from "node-iso11649";
import { customAlphabet } from "nanoid"; import { customAlphabet } from "nanoid";
import { isValidIBAN } from "ibantools";
import createAccordion from "../Accordion"; import createAccordion from "../Accordion";
import { import {
@ -40,9 +41,11 @@ import {
POSITION_TYPE_AGILE, POSITION_TYPE_AGILE,
POSITION_TYPE_QUANTITY, POSITION_TYPE_QUANTITY,
PrintType, PrintType,
printTypeSelectTitles,
printTypeTitles, printTypeTitles,
PRINT_TYPE_CONFIRMATION, PRINT_TYPE_CONFIRMATION,
PRINT_TYPE_INVOICE, PRINT_TYPE_INVOICE,
PRINT_TYPE_LETTER,
PRINT_TYPE_OFFER, PRINT_TYPE_OFFER,
StoreContext, StoreContext,
storeSchema, storeSchema,
@ -150,7 +153,7 @@ const SettingsOverlay: Component = () => {
/> />
<TextInput <TextInput
name={withPrefix("zip")} name={withPrefix("zip")}
label="Plz" label="PLZ"
type="number" type="number"
maxLength={16} maxLength={16}
min="0" min="0"
@ -349,6 +352,22 @@ const SettingsOverlay: Component = () => {
/> />
</div> </div>
<div class="col-span-2">
<TextInput
label="Titel"
labelMinWidth={fullWidthLabelWidth}
placeholder={
state.project.title
? undefined
: printTypeTitles[uiState.printType]
}
value={state.project.title || ""}
onInput={(evt) =>
setState("project", "title", evt.currentTarget.value)
}
/>
</div>
<div class="col-span-2"> <div class="col-span-2">
<TextArea <TextArea
label="Einleitung" label="Einleitung"
@ -513,9 +532,20 @@ const SettingsOverlay: Component = () => {
<div class="col-span-2"> <div class="col-span-2">
<TextInput <TextInput
required required
label="Iban" label="IBAN"
maxLength={50} maxLength={50}
value={localState.iban} value={localState.iban}
invalidate={(v) => {
// If the value is empty, the HTML5 "required" attribute will do its own validation, we can skip the IBAN check
if (v === "") {
return false;
}
return (
!isValidIBAN(v.replaceAll(" ", "")) &&
"Diese IBAN ist womöglich ungültig."
);
}}
onInput={(evt) => onInput={(evt) =>
setLocalState("iban", evt.currentTarget.value) setLocalState("iban", evt.currentTarget.value)
} }
@ -970,6 +1000,7 @@ const SettingsOverlay: Component = () => {
<For <For
each={ each={
[ [
PRINT_TYPE_LETTER,
PRINT_TYPE_OFFER, PRINT_TYPE_OFFER,
PRINT_TYPE_CONFIRMATION, PRINT_TYPE_CONFIRMATION,
PRINT_TYPE_INVOICE, PRINT_TYPE_INVOICE,
@ -981,7 +1012,7 @@ const SettingsOverlay: Component = () => {
value={type} value={type}
selected={type === uiState.printType} selected={type === uiState.printType}
> >
{printTypeTitles[type]} {printTypeSelectTitles[type]}
</option> </option>
)} )}
</For> </For>

@ -53,8 +53,10 @@ const encodeSwissQrInvoice = (
invoiceData: InvoiceData, invoiceData: InvoiceData,
{ type = "SPC" } = {} { type = "SPC" } = {}
) => { ) => {
const VERSION = "0220"; // 2.20 // This code implements version 2.2! But the guidelines require that this stays fixed as 2.0 ;)
const VERSION = "0200";
const CODING = 1; // utf-8 const CODING = 1; // utf-8
const LINE_BREAK = "\n";
const END_INDICATOR = "EPD"; const END_INDICATOR = "EPD";
const creditorType = invoiceData.creditor.type || "S"; const creditorType = invoiceData.creditor.type || "S";
@ -115,7 +117,7 @@ const encodeSwissQrInvoice = (
END_INDICATOR, END_INDICATOR,
] ]
.flat() .flat()
.join("\n"); .join(LINE_BREAK);
return invoice; return invoice;
}; };

@ -40,9 +40,11 @@ const EMPTY_OBJECT = {};
export const validateInput = function ({ export const validateInput = function ({
value: getValue, value: getValue,
invalidate: invalidate,
listen, listen,
}: { }: {
value?: () => any; value?: () => any;
invalidate?: (v: any) => string | boolean;
listen?: { listen?: {
input?: boolean; input?: boolean;
blur?: boolean; blur?: boolean;
@ -75,6 +77,19 @@ export const validateInput = function ({
// Update state on badInput (value stays unchanged on badInput), but only update the state once // Update state on badInput (value stays unchanged on badInput), but only update the state once
validity_.badInput != lastBadInput validity_.badInput != lastBadInput
) { ) {
if (invalidate) {
const invalidateResult = invalidate(nextValue);
if (invalidateResult) {
el.setCustomValidity(
invalidateResult === true
? "Input value is invalid."
: invalidateResult
);
} else {
el.setCustomValidity("");
}
}
name = name || el.name || el.id; name = name || el.name || el.id;
setValidity(validity_); setValidity(validity_);
lastBadInput = validity_.badInput; lastBadInput = validity_.badInput;

@ -61,4 +61,13 @@
.ignore-white-space .white-space { .ignore-white-space .white-space {
@apply whitespace-nowrap; @apply whitespace-nowrap;
} }
/*
Tailwind prose adds margin-bottom to * in some situations,
firefox renders margin-bottom on <br> elements, chrome doesnt,
margin-bottom on br is not ideal in all situations
*/
.prose br {
margin-bottom: 0;
}
} }

@ -28,9 +28,11 @@ import {
createStore, createStore,
createUiStore, createUiStore,
LocalStoreContext, LocalStoreContext,
printTypeSelectTitles,
printTypeTitles, printTypeTitles,
PRINT_TYPE_CONFIRMATION, PRINT_TYPE_CONFIRMATION,
PRINT_TYPE_INVOICE, PRINT_TYPE_INVOICE,
PRINT_TYPE_LETTER,
PRINT_TYPE_OFFER, PRINT_TYPE_OFFER,
StoreContext, StoreContext,
UiStoreContext, UiStoreContext,
@ -143,7 +145,7 @@ export default function Home() {
(state.project.projectNumber.length > 0 (state.project.projectNumber.length > 0
? `${state.project.projectNumber} - ` ? `${state.project.projectNumber} - `
: "") + : "") +
printTypeTitles[uiState.printType] printTypeSelectTitles[uiState.printType]
); );
const externalTitle = "Räppli - Web App für Schweizerische Rechnungen"; const externalTitle = "Räppli - Web App für Schweizerische Rechnungen";
@ -303,6 +305,10 @@ export default function Home() {
</Show> </Show>
); );
const pageTitle = createMemo(function () {
return state.project.title || printTypeTitles[uiState.printType];
});
return ( return (
<> <>
<Style type="text/css">{` <Style type="text/css">{`
@ -323,11 +329,22 @@ export default function Home() {
} }
} }
`}</Style> `}</Style>
<Show when={uiState.printType === PRINT_TYPE_LETTER}>
<Page>
<InnerPadding>
<PageHeader />
<Title>{pageTitle()}</Title>
<Preface />
<Conclusion />
<LufraiWatermark />
</InnerPadding>
</Page>
</Show>
<Show when={uiState.printType === PRINT_TYPE_OFFER}> <Show when={uiState.printType === PRINT_TYPE_OFFER}>
<Page> <Page>
<InnerPadding> <InnerPadding>
<PageHeader /> <PageHeader />
<Title>Offerte</Title> <Title>{pageTitle()}</Title>
<Preface /> <Preface />
<PositionsWithData /> <PositionsWithData />
<Conclusion /> <Conclusion />
@ -339,7 +356,7 @@ export default function Home() {
<Page> <Page>
<InnerPadding> <InnerPadding>
<PageHeader /> <PageHeader />
<Title>Auftragsbestätigung</Title> <Title>{pageTitle()}</Title>
<Preface /> <Preface />
<PositionsWithData /> <PositionsWithData />
<Conclusion /> <Conclusion />
@ -351,7 +368,7 @@ export default function Home() {
<Page> <Page>
<InnerPadding> <InnerPadding>
<PageHeader /> <PageHeader />
<Title>Rechnung</Title> <Title>{pageTitle()}</Title>
<Preface /> <Preface />
<PositionsWithData /> <PositionsWithData />
<Show when={localState.paymentTerms}> <Show when={localState.paymentTerms}>

@ -25,21 +25,29 @@ export const positionSchema = z.object({
export type Position = Infer<typeof positionSchema>; export type Position = Infer<typeof positionSchema>;
export const PRINT_TYPE_LETTER = "LETTER";
export const PRINT_TYPE_OFFER = "OFFER"; export const PRINT_TYPE_OFFER = "OFFER";
export const PRINT_TYPE_CONFIRMATION = "CONFIRMATION"; export const PRINT_TYPE_CONFIRMATION = "CONFIRMATION";
export const PRINT_TYPE_INVOICE = "INVOICE"; export const PRINT_TYPE_INVOICE = "INVOICE";
export type PrintType = export type PrintType =
| typeof PRINT_TYPE_LETTER
| typeof PRINT_TYPE_OFFER | typeof PRINT_TYPE_OFFER
| typeof PRINT_TYPE_CONFIRMATION | typeof PRINT_TYPE_CONFIRMATION
| typeof PRINT_TYPE_INVOICE; | typeof PRINT_TYPE_INVOICE;
export const printTypeTitles = { export const printTypeTitles = {
[PRINT_TYPE_LETTER]: "Brief",
[PRINT_TYPE_OFFER]: "Offerte", [PRINT_TYPE_OFFER]: "Offerte",
[PRINT_TYPE_CONFIRMATION]: "Bestätigung", [PRINT_TYPE_CONFIRMATION]: "Auftragsbestätigung",
[PRINT_TYPE_INVOICE]: "Rechnung", [PRINT_TYPE_INVOICE]: "Rechnung",
}; };
export const printTypeSelectTitles = {
...printTypeTitles,
[PRINT_TYPE_CONFIRMATION]: "Bestätigung",
};
export const createUiStore = () => export const createUiStore = () =>
createStore_({ createStore_({
lastSaved: 0, lastSaved: 0,
@ -63,6 +71,7 @@ export const storeSchema = z.object({
deliveryNumber: z.string(), deliveryNumber: z.string(),
deliveryDate: z.number().optional(), deliveryDate: z.number().optional(),
date: z.number(), date: z.number(),
title: z.string().optional(),
preface: z.string().optional(), preface: z.string().optional(),
conclusion: z.string().optional(), conclusion: z.string().optional(),
}), }),

Loading…
Cancel
Save