Feature: SEPA-Export, Push-Notifications, Onboarding + vollständige UI
- Phosphor Icons (Icon.svelte, svg-Registry) - Schema-Abgleich: alle Felder zwischen PB-Migrations und types.ts konsistent - Stripe entfernt, SEPA pain.008 XML-Export implementiert (sepa.ts) - Beiträge: vollständiges CRUD + SEPA-Einzug-Sheet mit Vorschau - Termine: vollständiges CRUD (upcoming/vergangen, datetime-local) - Mitglieder: Formulare um alle Felder erweitert (Adresse, SEPA-Mandat, Notizen) - Nachrichten: Brevo E-Mail via PocketBase-Hook, UI mit Gruppen-Filter - Push-Notifications: VAPID, Custom Service Worker (injectManifest), Subscribe/Send API-Routen, automatische Subscription nach Login - Onboarding: 3-Schritt-Flow für neue Vereine, Guard im App-Layout - Makefile: .env wird vollständig zur DS übertragen
This commit is contained in:
parent
c2c4dfd518
commit
77c6f513b5
32 changed files with 3012 additions and 399 deletions
20
app/src/lib/components/Icon.svelte
Normal file
20
app/src/lib/components/Icon.svelte
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<script lang="ts">
|
||||
import { icons, type IconName } from '$lib/icons';
|
||||
|
||||
let { name, size = 24, class: cls = '' }: { name: IconName; size?: number; class?: string } = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
class="ph-icon {cls}"
|
||||
style="width:{size}px;height:{size}px;display:inline-flex;align-items:center;justify-content:center;"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{@html icons[name]}
|
||||
</span>
|
||||
|
||||
<style>
|
||||
.ph-icon :global(svg) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
15
app/src/lib/icons.ts
Normal file
15
app/src/lib/icons.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import house from './icons/house.svg?raw';
|
||||
import users from './icons/users.svg?raw';
|
||||
import calendar from './icons/calendar.svg?raw';
|
||||
import currencyEur from './icons/currency-eur.svg?raw';
|
||||
import envelope from './icons/envelope.svg?raw';
|
||||
|
||||
export const icons = {
|
||||
house,
|
||||
users,
|
||||
calendar,
|
||||
'currency-eur': currencyEur,
|
||||
envelope,
|
||||
} as const;
|
||||
|
||||
export type IconName = keyof typeof icons;
|
||||
1
app/src/lib/icons/calendar.svg
Normal file
1
app/src/lib/icons/calendar.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><rect x="40" y="40" width="176" height="176" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="176" y1="24" x2="176" y2="56" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="80" y1="24" x2="80" y2="56" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="40" y1="88" x2="216" y2="88" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="88 128 104 120 104 184" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M138.14,128a16,16,0,1,1,26.64,17.63L136,184h32" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
|
After Width: | Height: | Size: 980 B |
1
app/src/lib/icons/currency-eur.svg
Normal file
1
app/src/lib/icons/currency-eur.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="40" y1="112" x2="136" y2="112" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="40" y1="144" x2="120" y2="144" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M184,197.67A72,72,0,0,1,64,144V112A72,72,0,0,1,184,58.33" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
|
After Width: | Height: | Size: 561 B |
1
app/src/lib/icons/envelope.svg
Normal file
1
app/src/lib/icons/envelope.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><polyline points="224 56 128 144 32 56" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M32,56H224a0,0,0,0,1,0,0V192a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V56A0,0,0,0,1,32,56Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="110.55" y1="128" x2="34.47" y2="197.74" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="221.53" y1="197.74" x2="145.45" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
|
After Width: | Height: | Size: 743 B |
1
app/src/lib/icons/house.svg
Normal file
1
app/src/lib/icons/house.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M104,216V152h48v64h64V120a8,8,0,0,0-2.34-5.66l-80-80a8,8,0,0,0-11.32,0l-80,80A8,8,0,0,0,40,120v96Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
|
After Width: | Height: | Size: 321 B |
1
app/src/lib/icons/users.svg
Normal file
1
app/src/lib/icons/users.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><circle cx="84" cy="108" r="52" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M10.23,200a88,88,0,0,1,147.54,0" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M172,160a87.93,87.93,0,0,1,73.77,40" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M152.69,59.7A52,52,0,1,1,172,160" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
|
After Width: | Height: | Size: 675 B |
109
app/src/lib/sepa.ts
Normal file
109
app/src/lib/sepa.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
export interface SepaKopf {
|
||||
glaeubigerid: string;
|
||||
vereinIban: string;
|
||||
vereinBic: string;
|
||||
vereinName: string;
|
||||
einzugsdatum: string; // YYYY-MM-DD
|
||||
}
|
||||
|
||||
export interface SepaPosition {
|
||||
endToEndId: string;
|
||||
betrag: number;
|
||||
mandatsreferenz: string;
|
||||
mandatsdatum: string; // YYYY-MM-DD
|
||||
debitorName: string;
|
||||
debitorIban: string;
|
||||
debitorBic: string;
|
||||
verwendungszweck: string;
|
||||
}
|
||||
|
||||
function x(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function iban(raw: string): string {
|
||||
return raw.replace(/\s/g, '').toUpperCase();
|
||||
}
|
||||
|
||||
export function generatePain008(kopf: SepaKopf, positionen: SepaPosition[]): string {
|
||||
if (positionen.length === 0) throw new Error('Keine Positionen für den SEPA-Export.');
|
||||
|
||||
const now = new Date().toISOString().slice(0, 19);
|
||||
const msgId = `VH-${Date.now()}`;
|
||||
const ctrlSum = positionen.reduce((s, p) => s + p.betrag, 0).toFixed(2);
|
||||
const nbOfTxs = positionen.length;
|
||||
|
||||
const txXml = positionen.map((p) => `
|
||||
<DrctDbtTxInf>
|
||||
<PmtId><EndToEndId>${x(p.endToEndId)}</EndToEndId></PmtId>
|
||||
<InstdAmt Ccy="EUR">${p.betrag.toFixed(2)}</InstdAmt>
|
||||
<DrctDbtTx>
|
||||
<MndtRltdInf>
|
||||
<MndtId>${x(p.mandatsreferenz)}</MndtId>
|
||||
<DtOfSgntr>${p.mandatsdatum}</DtOfSgntr>
|
||||
</MndtRltdInf>
|
||||
<CdtrSchmeId><Id><PrvtId><Othr>
|
||||
<Id>${x(kopf.glaeubigerid)}</Id>
|
||||
<SchmeNm><Prtry>SEPA</Prtry></SchmeNm>
|
||||
</Othr></PrvtId></Id></CdtrSchmeId>
|
||||
</DrctDbtTx>
|
||||
<DbtrAgt><FinInstnId>${p.debitorBic ? `<BIC>${x(p.debitorBic)}</BIC>` : '<Othr><Id>NOTPROVIDED</Id></Othr>'}</FinInstnId></DbtrAgt>
|
||||
<Dbtr><Nm>${x(p.debitorName)}</Nm></Dbtr>
|
||||
<DbtrAcct><Id><IBAN>${iban(p.debitorIban)}</IBAN></Id></DbtrAcct>
|
||||
<RmtInf><Ustrd>${x(p.verwendungszweck.slice(0, 140))}</Ustrd></RmtInf>
|
||||
</DrctDbtTxInf>`).join('');
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.008.003.02"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<CstmrDrctDbtInitn>
|
||||
<GrpHdr>
|
||||
<MsgId>${msgId}</MsgId>
|
||||
<CreDtTm>${now}</CreDtTm>
|
||||
<NbOfTxs>${nbOfTxs}</NbOfTxs>
|
||||
<CtrlSum>${ctrlSum}</CtrlSum>
|
||||
<InitgPty><Nm>${x(kopf.vereinName)}</Nm></InitgPty>
|
||||
</GrpHdr>
|
||||
<PmtInf>
|
||||
<PmtInfId>${msgId}-PI</PmtInfId>
|
||||
<PmtMtd>DD</PmtMtd>
|
||||
<NbOfTxs>${nbOfTxs}</NbOfTxs>
|
||||
<CtrlSum>${ctrlSum}</CtrlSum>
|
||||
<PmtTpInf>
|
||||
<SvcLvl><Cd>SEPA</Cd></SvcLvl>
|
||||
<LclInstrm><Cd>CORE</Cd></LclInstrm>
|
||||
<SeqTp>RCUR</SeqTp>
|
||||
</PmtTpInf>
|
||||
<ReqdColltnDt>${kopf.einzugsdatum}</ReqdColltnDt>
|
||||
<Cdtr><Nm>${x(kopf.vereinName)}</Nm></Cdtr>
|
||||
<CdtrAcct><Id><IBAN>${iban(kopf.vereinIban)}</IBAN></Id></CdtrAcct>
|
||||
<CdtrAgt><FinInstnId><BIC>${x(kopf.vereinBic)}</BIC></FinInstnId></CdtrAgt>
|
||||
<CdtrSchmeId><Id><PrvtId><Othr>
|
||||
<Id>${x(kopf.glaeubigerid)}</Id>
|
||||
<SchmeNm><Prtry>SEPA</Prtry></SchmeNm>
|
||||
</Othr></PrvtId></Id></CdtrSchmeId>${txXml}
|
||||
</PmtInf>
|
||||
</CstmrDrctDbtInitn>
|
||||
</Document>`;
|
||||
}
|
||||
|
||||
export function downloadXml(xml: string, dateiname: string) {
|
||||
const blob = new Blob([xml], { type: 'application/xml;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = dateiname;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
export function minEinzugsdatum(): string {
|
||||
// SEPA CORE RCUR: mindestens 2 Bankarbeitstage Vorlaufzeit (vereinfacht: +3 Tage)
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() + 3);
|
||||
return d.toISOString().slice(0, 10);
|
||||
}
|
||||
|
|
@ -1,18 +1,20 @@
|
|||
export type Plan = 'free' | 'starter' | 'wachstum' | 'verband';
|
||||
export type MitgliedStatus = 'aktiv' | 'passiv' | 'ausgetreten';
|
||||
export type EinzugStatus = 'ausstehend' | 'eingezogen' | 'fehlgeschlagen';
|
||||
export type Rhythmus = 'monatlich' | 'quartalsweise' | 'halbjaehrlich' | 'jaehrlich';
|
||||
export type EinzugStatus = 'ausstehend' | 'eingezogen' | 'fehlgeschlagen' | 'storniert';
|
||||
export type Rhythmus = 'monatlich' | 'quartalsweise' | 'halbjaehrlich' | 'jaehrlich' | 'einmalig';
|
||||
|
||||
export interface Verein {
|
||||
id: string;
|
||||
name: string;
|
||||
adresse: string;
|
||||
plz: string;
|
||||
ort: string;
|
||||
bundesland: string;
|
||||
adresse?: string;
|
||||
plz?: string;
|
||||
ort?: string;
|
||||
bundesland?: string;
|
||||
plan: Plan;
|
||||
stripe_customer_id?: string;
|
||||
dosb_mitglied: boolean;
|
||||
glaeubigerid?: string;
|
||||
iban?: string;
|
||||
bic?: string;
|
||||
}
|
||||
|
||||
export interface Mitglied {
|
||||
|
|
@ -20,19 +22,21 @@ export interface Mitglied {
|
|||
verein_id: string;
|
||||
vorname: string;
|
||||
nachname: string;
|
||||
email: string;
|
||||
email?: string;
|
||||
telefon?: string;
|
||||
geburtsdatum?: string;
|
||||
eintrittsdatum: string;
|
||||
eintrittsdatum?: string;
|
||||
austrittsdatum?: string;
|
||||
strasse: string;
|
||||
plz: string;
|
||||
ort: string;
|
||||
strasse?: string;
|
||||
plz?: string;
|
||||
ort?: string;
|
||||
iban?: string;
|
||||
bic?: string;
|
||||
gruppe_ids: string[];
|
||||
status: MitgliedStatus;
|
||||
notizen?: string;
|
||||
mandatsreferenz?: string;
|
||||
mandatsdatum?: string;
|
||||
}
|
||||
|
||||
export interface Gruppe {
|
||||
|
|
@ -53,13 +57,11 @@ export interface Beitrag {
|
|||
|
||||
export interface Einzug {
|
||||
id: string;
|
||||
verein_id: string;
|
||||
mitglied_id: string;
|
||||
beitrag_id: string;
|
||||
betrag: number;
|
||||
faelligkeitsdatum: string;
|
||||
faellig_am?: string;
|
||||
status: EinzugStatus;
|
||||
stripe_payment_intent_id?: string;
|
||||
}
|
||||
|
||||
export interface Termin {
|
||||
|
|
@ -80,5 +82,5 @@ export interface Nachricht {
|
|||
betreff: string;
|
||||
text: string;
|
||||
gruppe_ids: string[];
|
||||
gesendet_am: string;
|
||||
gesendet_am?: string;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue