From ba28c54ae88bbb291aa35c26960c622b40f1de43 Mon Sep 17 00:00:00 2001 From: seaznCode Date: Fri, 20 Mar 2026 16:27:49 +0100 Subject: [PATCH 1/3] feat: add required fields for signing city and signature to subscription process --- .../summary/components/SignaturePad.tsx | 14 ++++++---- .../summary/hooks/subscribeAbo.ts | 8 ++++++ src/app/coffee-abonnements/summary/page.tsx | 27 +++++++++++++++---- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/app/coffee-abonnements/summary/components/SignaturePad.tsx b/src/app/coffee-abonnements/summary/components/SignaturePad.tsx index c81c7d3..7d743f8 100644 --- a/src/app/coffee-abonnements/summary/components/SignaturePad.tsx +++ b/src/app/coffee-abonnements/summary/components/SignaturePad.tsx @@ -6,9 +6,11 @@ type Props = { value: string onChange: (dataUrl: string) => void className?: string + required?: boolean + error?: string | null } -export default function SignaturePad({ value, onChange, className }: Props) { +export default function SignaturePad({ value, onChange, className, required = false, error = null }: Props) { const canvasRef = useRef(null) const isDrawing = useRef(false) @@ -137,7 +139,9 @@ export default function SignaturePad({ value, onChange, className }: Props) { return (
-

Signature

+

+ Signature{required ? ' *' : ''} +

-
+
-

- {value ? 'Signature captured.' : 'Draw your signature in the box.'} +

+ {error || (value ? 'Signature captured.' : 'Draw your signature in the box.')}

) diff --git a/src/app/coffee-abonnements/summary/hooks/subscribeAbo.ts b/src/app/coffee-abonnements/summary/hooks/subscribeAbo.ts index 1b6637b..4f6537c 100644 --- a/src/app/coffee-abonnements/summary/hooks/subscribeAbo.ts +++ b/src/app/coffee-abonnements/summary/hooks/subscribeAbo.ts @@ -70,6 +70,14 @@ export async function subscribeAbo(input: SubscribeAboInput) { throw new Error(`Missing required fields: ${missing.join(', ')}`) } + if (typeof input.signingCity !== 'string' || input.signingCity.trim() === '') { + throw new Error('signingCity is required') + } + + if (typeof input.signatureDataUrl !== 'string' || input.signatureDataUrl.trim() === '') { + throw new Error('signatureDataUrl is required') + } + const body: any = { billing_interval: input.billing_interval ?? 'month', interval_count: input.interval_count ?? 1, diff --git a/src/app/coffee-abonnements/summary/page.tsx b/src/app/coffee-abonnements/summary/page.tsx index 5ed3dad..f87ad1c 100644 --- a/src/app/coffee-abonnements/summary/page.tsx +++ b/src/app/coffee-abonnements/summary/page.tsx @@ -510,12 +510,16 @@ export default function SummaryPage() { const hasRequiredSelfFields = requiredSelfFields.every(k => String(form[k]).trim() !== '') const hasRequiredInvoiceFields = form.invoiceSameAsShipping || form.invoiceEmail.trim() !== '' + const hasSigningCity = form.signingCity.trim() !== '' + const hasSignature = signatureDataUrl.trim() !== '' const canSubmit = selectedEntries.length > 0 && totalPacks === requiredPacks && hasRequiredSelfFields && - hasRequiredInvoiceFields; + hasRequiredInvoiceFields && + hasSigningCity && + hasSignature; const backToSelection = () => router.push('/coffee-abonnements'); @@ -527,6 +531,16 @@ export default function SummaryPage() { return } + if (!hasSigningCity) { + setSubmitError('Signing city is required.') + return + } + + if (!hasSignature) { + setSubmitError('Signature is required.') + return + } + setSubmitError(null) setSubmitLoading(true) try { @@ -781,10 +795,13 @@ export default function SummaryPage() {
- - + + + {!hasSigningCity && submitError && ( +

Ort ist erforderlich.

+ )}
- +
@@ -838,7 +855,7 @@ export default function SummaryPage() { {!canSubmit && (

- Please select coffees and fill all required buyer fields. + Please select coffees and fill all required buyer fields, signing city, and signature.

)} -- 2.39.5 From 7a8801274fa63443361105995774ff754d218e76 Mon Sep 17 00:00:00 2001 From: seaznCode Date: Tue, 7 Apr 2026 16:49:56 +0200 Subject: [PATCH 2/3] feat: add email report functionality to finance management page --- src/app/admin/finance-management/page.tsx | 84 +++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/src/app/admin/finance-management/page.tsx b/src/app/admin/finance-management/page.tsx index 96d72ae..1d4db21 100644 --- a/src/app/admin/finance-management/page.tsx +++ b/src/app/admin/finance-management/page.tsx @@ -18,6 +18,10 @@ export default function FinanceManagementPage() { const [diagData, setDiagData] = useState(null) const [selectedInvoice, setSelectedInvoice] = useState(null) const [detailModalOpen, setDetailModalOpen] = useState(false) + const [emailDialogOpen, setEmailDialogOpen] = useState(false) + const [reportEmail, setReportEmail] = useState('') + const [sendingReport, setSendingReport] = useState(false) + const [reportMsg, setReportMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null) // NEW: fetch invoices from backend const { @@ -116,6 +120,39 @@ export default function FinanceManagementPage() { URL.revokeObjectURL(url) } + const sendEmailReport = async () => { + if (!reportEmail.trim()) return + setReportMsg(null) + setSendingReport(true) + try { + const base = process.env.NEXT_PUBLIC_API_BASE_URL || '' + const res = await fetch(`${base}/api/admin/invoices/email-report`, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), + }, + body: JSON.stringify({ + email: reportEmail.trim(), + from: billFilter.from || undefined, + to: billFilter.to || undefined, + }), + }) + const body = await res.json().catch(() => ({})) + if (!res.ok || body?.success === false) { + throw new Error(body?.message || `Request failed (${res.status})`) + } + setReportMsg({ type: 'success', text: `Report sent to ${reportEmail.trim()} (${body.data?.sentCount ?? 0} paid invoice(s)).` }) + setEmailDialogOpen(false) + setReportEmail('') + } catch (e: any) { + setReportMsg({ type: 'error', text: e?.message || 'Failed to send email report.' }) + } finally { + setSendingReport(false) + } + } + return (
@@ -182,6 +219,7 @@ export default function FinanceManagementPage() {

Invoices

+ @@ -222,6 +260,11 @@ export default function FinanceManagementPage() {
+ {reportMsg && ( +
+ {reportMsg.text} +
+ )} {invError && (
{invError} @@ -366,6 +409,47 @@ export default function FinanceManagementPage() { onExport={(inv) => exportInvoice(inv)} /> )} + + {/* Email Report Dialog */} + {emailDialogOpen && ( +
+
+

Send Email Report

+
+ Only paid invoices will be included in the report, regardless of the status filter. + {(billFilter.from || billFilter.to) && ( + The current date range filter ({billFilter.from || '…'} – {billFilter.to || '…'}) will be applied. + )} +
+ + setReportEmail(e.target.value)} + placeholder="email@example.com" + className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm text-gray-900 placeholder:text-gray-400 focus:ring-2 focus:ring-blue-900 focus:border-transparent" + autoFocus + onKeyDown={e => { if (e.key === 'Enter' && !sendingReport) sendEmailReport() }} + /> +
+ + +
+
+
+ )}
-- 2.39.5 From 4ff36d172818455659f73f67bef2c8a89a0b2088 Mon Sep 17 00:00:00 2001 From: seaznCode Date: Tue, 7 Apr 2026 17:04:05 +0200 Subject: [PATCH 3/3] feat: add PDF viewing functionality for invoices in finance management --- src/app/admin/finance-management/page.tsx | 36 ++++++++++++++++++- .../profile/components/financeInvoices.tsx | 21 ++++++++--- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/src/app/admin/finance-management/page.tsx b/src/app/admin/finance-management/page.tsx index 1d4db21..9b559ba 100644 --- a/src/app/admin/finance-management/page.tsx +++ b/src/app/admin/finance-management/page.tsx @@ -120,6 +120,33 @@ export default function FinanceManagementPage() { URL.revokeObjectURL(url) } + const [pdfLoading, setPdfLoading] = useState(null) + + const viewInvoicePdf = async (inv: AdminInvoice) => { + setPdfLoading(inv.id) + try { + const base = process.env.NEXT_PUBLIC_API_BASE_URL || '' + const res = await fetch(`${base}/api/invoices/${inv.id}/pdf`, { + method: 'GET', + credentials: 'include', + headers: { + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), + }, + }) + if (!res.ok) { + const body = await res.json().catch(() => ({})) + throw new Error(body?.message || `Failed to load PDF (${res.status})`) + } + const blob = await res.blob() + const blobUrl = URL.createObjectURL(blob) + window.open(blobUrl, '_blank', 'noopener,noreferrer') + } catch (e: any) { + setReportMsg({ type: 'error', text: e?.message || 'Failed to load invoice PDF.' }) + } finally { + setPdfLoading(null) + } + } + const sendEmailReport = async () => { if (!reportEmail.trim()) return setReportMsg(null) @@ -385,11 +412,18 @@ export default function FinanceManagementPage() { + diff --git a/src/app/profile/components/financeInvoices.tsx b/src/app/profile/components/financeInvoices.tsx index f055ba7..22ca016 100644 --- a/src/app/profile/components/financeInvoices.tsx +++ b/src/app/profile/components/financeInvoices.tsx @@ -28,8 +28,10 @@ const isAbsUrl = (url: string) => /^https?:\/\//i.test(url) const resolveInvoiceUrl = (invoice: AboInvoice) => { const raw = invoice.pdfUrl || invoice.downloadUrl || invoice.htmlUrl || invoice.fileUrl - if (!raw) return null - return isAbsUrl(raw) ? raw : `${BASE_URL}${raw.startsWith('/') ? '' : '/'}${raw}` + if (raw) return isAbsUrl(raw) ? raw : `${BASE_URL}${raw.startsWith('/') ? '' : '/'}${raw}` + // Fallback: use the backend PDF proxy endpoint if an id is available + if (invoice.id) return `${BASE_URL}/api/invoices/${invoice.id}/pdf` + return null } type UiLifecycleStatus = 'issued' | 'ongoing' | 'finished' | 'pause' | 'cancelled' @@ -80,14 +82,25 @@ export default function FinanceInvoices({ abonementId }: Props) { const [busyId, setBusyId] = React.useState(null) const [actionError, setActionError] = React.useState(null) - const onView = (invoice: AboInvoice) => { + const onView = async (invoice: AboInvoice) => { setActionError(null) const url = resolveInvoiceUrl(invoice) if (!url) { setActionError('No view URL is available for this invoice.') return } - window.open(url, '_blank', 'noopener,noreferrer') + setBusyId(invoice.id) + try { + const res = await authFetch(url, { method: 'GET' }) + if (!res.ok) throw new Error(`Failed to load PDF: ${res.status}`) + const blob = await res.blob() + const blobUrl = URL.createObjectURL(blob) + window.open(blobUrl, '_blank', 'noopener,noreferrer') + } catch (e: any) { + setActionError(e?.message || 'Failed to load invoice PDF.') + } finally { + setBusyId(null) + } } const onDownload = async (invoice: AboInvoice) => { -- 2.39.5