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.
## [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)

@ -25,6 +25,12 @@ npm run dev
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
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",
"version": "1.3.0",
"version": "1.4.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "rappli",
"version": "1.3.0",
"version": "1.4.0",
"license": "MIT",
"dependencies": {
"solid-start": "v0.1.0-alpha.89",
@ -30,6 +30,7 @@
"daisyui": "^2.20.0",
"date-fns": "^2.29.1",
"froebel": "^0.18.0",
"ibantools": "^4.1.6",
"myzod": "^1.8.7",
"nanoid": "^4.0.0",
"node-iso11649": "^2.1.2",
@ -4249,6 +4250,12 @@
"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": {
"version": "4.0.0",
"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",
"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": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",

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

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

@ -17,6 +17,7 @@ import z from "myzod";
import Big from "big.js";
import { generate } from "node-iso11649";
import { customAlphabet } from "nanoid";
import { isValidIBAN } from "ibantools";
import createAccordion from "../Accordion";
import {
@ -40,9 +41,11 @@ import {
POSITION_TYPE_AGILE,
POSITION_TYPE_QUANTITY,
PrintType,
printTypeSelectTitles,
printTypeTitles,
PRINT_TYPE_CONFIRMATION,
PRINT_TYPE_INVOICE,
PRINT_TYPE_LETTER,
PRINT_TYPE_OFFER,
StoreContext,
storeSchema,
@ -150,7 +153,7 @@ const SettingsOverlay: Component = () => {
/>
<TextInput
name={withPrefix("zip")}
label="Plz"
label="PLZ"
type="number"
maxLength={16}
min="0"
@ -349,6 +352,22 @@ const SettingsOverlay: Component = () => {
/>
</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">
<TextArea
label="Einleitung"
@ -513,9 +532,20 @@ const SettingsOverlay: Component = () => {
<div class="col-span-2">
<TextInput
required
label="Iban"
label="IBAN"
maxLength={50}
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) =>
setLocalState("iban", evt.currentTarget.value)
}
@ -970,6 +1000,7 @@ const SettingsOverlay: Component = () => {
<For
each={
[
PRINT_TYPE_LETTER,
PRINT_TYPE_OFFER,
PRINT_TYPE_CONFIRMATION,
PRINT_TYPE_INVOICE,
@ -981,7 +1012,7 @@ const SettingsOverlay: Component = () => {
value={type}
selected={type === uiState.printType}
>
{printTypeTitles[type]}
{printTypeSelectTitles[type]}
</option>
)}
</For>

@ -53,8 +53,10 @@ const encodeSwissQrInvoice = (
invoiceData: InvoiceData,
{ 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 LINE_BREAK = "\n";
const END_INDICATOR = "EPD";
const creditorType = invoiceData.creditor.type || "S";
@ -115,7 +117,7 @@ const encodeSwissQrInvoice = (
END_INDICATOR,
]
.flat()
.join("\n");
.join(LINE_BREAK);
return invoice;
};

@ -40,9 +40,11 @@ const EMPTY_OBJECT = {};
export const validateInput = function ({
value: getValue,
invalidate: invalidate,
listen,
}: {
value?: () => any;
invalidate?: (v: any) => string | boolean;
listen?: {
input?: 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
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;
setValidity(validity_);
lastBadInput = validity_.badInput;

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

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

Loading…
Cancel
Save