diff --git a/apps/web/app/[locale]/home/[account]/finance/sepa/[batchId]/page.tsx b/apps/web/app/[locale]/home/[account]/finance/sepa/[batchId]/page.tsx index fccf4cc51..001296992 100644 --- a/apps/web/app/[locale]/home/[account]/finance/sepa/[batchId]/page.tsx +++ b/apps/web/app/[locale]/home/[account]/finance/sepa/[batchId]/page.tsx @@ -1,13 +1,13 @@ import Link from 'next/link'; -import { ArrowLeft, Download } from 'lucide-react'; +import { ArrowLeft } from 'lucide-react'; import { getTranslations } from 'next-intl/server'; import { createFinanceApi } from '@kit/finance/api'; +import { SepaBatchActions } from '@kit/finance/components'; import { formatDate } from '@kit/shared/dates'; import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { Badge } from '@kit/ui/badge'; -import { Button } from '@kit/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; import { AccountNotFound } from '~/components/account-not-found'; @@ -124,10 +124,12 @@ export default async function SepaBatchDetailPage({ params }: PageProps) {
- +
diff --git a/apps/web/app/[locale]/home/[account]/finance/sepa/page.tsx b/apps/web/app/[locale]/home/[account]/finance/sepa/page.tsx index 49cc32162..6bb19431d 100644 --- a/apps/web/app/[locale]/home/[account]/finance/sepa/page.tsx +++ b/apps/web/app/[locale]/home/[account]/finance/sepa/page.tsx @@ -108,7 +108,7 @@ export default async function SepaPage({ params }: PageProps) { {batches.map((batch: Record) => ( {String(batch.description ?? '—')} diff --git a/packages/features/finance/package.json b/packages/features/finance/package.json index 85c442426..0c2090a3b 100644 --- a/packages/features/finance/package.json +++ b/packages/features/finance/package.json @@ -33,5 +33,8 @@ "react": "catalog:", "react-hook-form": "catalog:", "zod": "catalog:" + }, + "dependencies": { + "lucide-react": "catalog:" } -} +} \ No newline at end of file diff --git a/packages/features/finance/src/components/index.ts b/packages/features/finance/src/components/index.ts index d6b6b857c..c4be31bbd 100644 --- a/packages/features/finance/src/components/index.ts +++ b/packages/features/finance/src/components/index.ts @@ -1,2 +1,3 @@ export { CreateInvoiceForm } from './create-invoice-form'; export { CreateSepaBatchForm } from './create-sepa-batch-form'; +export { SepaBatchActions } from './sepa-batch-actions'; diff --git a/packages/features/finance/src/components/sepa-batch-actions.tsx b/packages/features/finance/src/components/sepa-batch-actions.tsx new file mode 100644 index 000000000..8ca314f97 --- /dev/null +++ b/packages/features/finance/src/components/sepa-batch-actions.tsx @@ -0,0 +1,377 @@ +'use client'; + +import { useAction } from 'next-safe-action/hooks'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; + +import { + Download, + UserPlus, + Plus, + Loader2, + AlertTriangle, +} from 'lucide-react'; + +import { Button } from '@kit/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@kit/ui/dialog'; +import { Input } from '@kit/ui/input'; +import { Label } from '@kit/ui/label'; +import { toast } from '@kit/ui/sonner'; + +import { + populateBatchFromMembers, + addSepaItem, + generateSepaXml, +} from '../server/actions/finance-actions'; + +interface SepaBatchActionsProps { + batchId: string; + accountId: string; + batchStatus: string; + itemCount: number; +} + +/** + * Client-side toolbar for SEPA batch detail page. + * Provides: populate from members, add single item, generate XML. + */ +export function SepaBatchActions({ + batchId, + accountId, + batchStatus, + itemCount, +}: SepaBatchActionsProps) { + const router = useRouter(); + const isDraft = batchStatus === 'draft'; + + // ── Populate from members ── + const populateAction = useAction(populateBatchFromMembers, { + onSuccess: ({ data }) => { + const count = data?.data?.addedCount ?? 0; + if (count > 0) { + toast.success(`${count} Mitglieder hinzugefügt`, { + description: 'Positionen wurden aus der Mitgliederliste erstellt.', + }); + } else { + toast.info('Keine Mitglieder gefunden', { + description: + 'Keine aktiven Mitglieder mit SEPA-Mandat und Beitragskategorie vorhanden.', + }); + } + router.refresh(); + }, + onError: () => { + toast.error('Fehler beim Hinzufügen', { + description: 'Bitte versuchen Sie es erneut.', + }); + }, + }); + + // ── Add single item ── + const [addOpen, setAddOpen] = useState(false); + const [singleItem, setSingleItem] = useState({ + debtorName: '', + debtorIban: '', + amount: '', + remittanceInfo: 'Mitgliedsbeitrag', + }); + + const addItemAction = useAction(addSepaItem, { + onSuccess: () => { + toast.success('Position hinzugefügt'); + setAddOpen(false); + setSingleItem({ + debtorName: '', + debtorIban: '', + amount: '', + remittanceInfo: 'Mitgliedsbeitrag', + }); + router.refresh(); + }, + onError: () => { + toast.error('Fehler beim Hinzufügen der Position'); + }, + }); + + // ── Generate XML ── + const [xmlOpen, setXmlOpen] = useState(false); + const [creditor, setCreditor] = useState({ + creditorName: '', + creditorIban: '', + creditorBic: '', + creditorId: '', + }); + + const xmlAction = useAction(generateSepaXml, { + onSuccess: ({ data }) => { + if (data?.data?.content) { + // Trigger download + const blob = new Blob([data.data.content], { + type: 'application/xml', + }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = data.data.filename; + a.click(); + URL.revokeObjectURL(url); + toast.success('SEPA-XML heruntergeladen'); + setXmlOpen(false); + } + }, + onError: () => { + toast.error('Fehler beim Generieren der XML-Datei'); + }, + }); + + return ( +
+ {/* Populate from members — main CTA for draft batches */} + {isDraft && ( + + )} + + {/* Add single position manually */} + {isDraft && ( + + + + + + + Position manuell hinzufügen + + Geben Sie die Daten des Zahlungspflichtigen ein. + + +
+
+ + + setSingleItem((s) => ({ + ...s, + debtorName: e.target.value, + })) + } + /> +
+
+ + + setSingleItem((s) => ({ + ...s, + debtorIban: e.target.value, + })) + } + /> +
+
+ + + setSingleItem((s) => ({ + ...s, + amount: e.target.value, + })) + } + /> +
+
+ + + setSingleItem((s) => ({ + ...s, + remittanceInfo: e.target.value, + })) + } + /> +
+
+ + + + +
+
+ )} + + {/* Generate & download XML */} + + + + + + + SEPA-XML generieren + + Geben Sie die Gläubiger-Daten Ihres Vereins ein. Diese werden im + SEPA-XML als Zahlungsempfänger verwendet. + + +
+
+ + + setCreditor((s) => ({ ...s, creditorName: e.target.value })) + } + /> +
+
+ + + setCreditor((s) => ({ ...s, creditorIban: e.target.value })) + } + /> +
+
+ + + setCreditor((s) => ({ ...s, creditorBic: e.target.value })) + } + /> +
+
+ + + setCreditor((s) => ({ ...s, creditorId: e.target.value })) + } + /> +

+ Die Gläubiger-Identifikationsnummer erhalten Sie bei der + Deutschen Bundesbank. +

+
+
+ {itemCount === 0 && ( +
+ + + Keine Positionen vorhanden. Bitte fügen Sie zuerst Mitglieder + hinzu. + +
+ )} + + + + +
+
+
+ ); +}