diff --git a/frontend/src/components/admin/SetupConfigStep.tsx b/frontend/src/components/admin/SetupConfigStep.tsx
new file mode 100644
index 00000000..b6a3a200
--- /dev/null
+++ b/frontend/src/components/admin/SetupConfigStep.tsx
@@ -0,0 +1,153 @@
+import React, { useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { toast } from 'react-toastify';
+import { ShieldAlert } from 'lucide-react';
+
+import { Button, Input } from '../common';
+import type { FeatureKey } from '../../services/featureFlags.service';
+import { businessProfileService } from '../../services/businessProfile.service';
+import { emailService, type EmailConfig } from '../../services/email.service';
+
+// Features that need working SMTP to deliver anything.
+const EMAIL_FEATURES: FeatureKey[] = ['reminderEmails', 'incomingMail', 'whatsapp', 'bills'];
+
+interface Props {
+ selectedFeatures: Set;
+ onDone: () => void;
+}
+
+// Lean per-feature config, shown after the "How will you use PicPeak?" step.
+// Only the sections a selected feature actually needs are rendered; everything
+// else keeps its seeded defaults and is tunable later in Settings. Saving is
+// best-effort per section — a failure never traps the user on setup.
+export const SetupConfigStep: React.FC = ({ selectedFeatures, onDone }) => {
+ const { t } = useTranslation();
+ const showInvoicing = selectedFeatures.has('bills');
+ const showEmail = EMAIL_FEATURES.some((f) => selectedFeatures.has(f));
+ const [saving, setSaving] = useState(false);
+
+ const [inv, setInv] = useState({
+ companyName: '', addressLine1: '', postalCode: '', city: '', countryCode: '',
+ vatId: '', taxId: '', defaultCurrency: 'CHF', iban: '',
+ });
+ const [mail, setMail] = useState({
+ smtp_host: '', smtp_port: '587', smtp_user: '', smtp_pass: '', from_email: '', from_name: '',
+ });
+
+ const invField = (k: keyof typeof inv) => (e: React.ChangeEvent) =>
+ setInv((p) => ({ ...p, [k]: e.target.value }));
+ const mailField = (k: keyof typeof mail) => (e: React.ChangeEvent) =>
+ setMail((p) => ({ ...p, [k]: e.target.value }));
+
+ const finish = async () => {
+ setSaving(true);
+ try {
+ // Invoicing: only persist if they actually started filling it in.
+ if (showInvoicing && inv.companyName.trim()) {
+ await businessProfileService.update({
+ companyName: inv.companyName.trim(),
+ addressLine1: inv.addressLine1.trim(),
+ postalCode: inv.postalCode.trim(),
+ city: inv.city.trim(),
+ countryCode: inv.countryCode.trim(),
+ vatId: inv.vatId.trim(),
+ taxId: inv.taxId.trim(),
+ defaultCurrency: inv.defaultCurrency.trim() || 'CHF',
+ });
+ if (inv.iban.trim()) {
+ await businessProfileService.createBankAccount({
+ iban: inv.iban.replace(/\s+/g, ''),
+ accountHolder: inv.companyName.trim(),
+ currency: inv.defaultCurrency.trim() || 'CHF',
+ isDefault: true,
+ });
+ }
+ }
+ // Email: only persist if a host was entered.
+ if (showEmail && mail.smtp_host.trim()) {
+ const port = parseInt(mail.smtp_port, 10) || 587;
+ const config: EmailConfig = {
+ smtp_host: mail.smtp_host.trim(),
+ smtp_port: port,
+ smtp_secure: port === 465,
+ smtp_user: mail.smtp_user.trim(),
+ smtp_pass: mail.smtp_pass,
+ from_email: mail.from_email.trim(),
+ from_name: mail.from_name.trim(),
+ tls_reject_unauthorized: true,
+ };
+ await emailService.updateConfig(config);
+ }
+ } catch (_) {
+ toast.warn(t('setup.config.saveFailed', 'Some settings could not be saved — you can finish them in Settings.'));
+ } finally {
+ setSaving(false);
+ onDone();
+ }
+ };
+
+ return (
+
+
+ {t('setup.config.intro', 'A few details for the features you picked. Anything you skip keeps its default and can be set later in Settings.')}
+
+
+ {showInvoicing && (
+
+ )}
+
+ {showEmail && (
+
+ )}
+
+
+
+ {t('setup.config.skip', 'Skip for now')}
+
+
+ {t('setup.config.finish', 'Finish setup')}
+
+
+
+ );
+};
+
+SetupConfigStep.displayName = 'SetupConfigStep';
diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json
index f704d13e..3b761a57 100644
--- a/frontend/src/i18n/locales/de.json
+++ b/frontend/src/i18n/locales/de.json
@@ -3509,6 +3509,32 @@
"restoreEntry": "Wechsel von einer anderen PicPeak-Instanz?",
"restoreEntryHint": "Stellen Sie stattdessen ein .picpeak-Backup wieder her, anstatt neu einzurichten.",
"restoreIntro": "Laden Sie ein .picpeak-Backup hoch, um eine andere Instanz auf diese zu klonen. Dies ersetzt alles außer dem gerade erstellten Konto.",
+ "config": {
+ "subtitle": "Richten Sie Ihre Funktionen ein",
+ "intro": "Einige Angaben zu den gewählten Funktionen. Was Sie überspringen, behält den Standard und kann später in den Einstellungen festgelegt werden.",
+ "invoicing": "Rechnungsdaten",
+ "invoicingDisclaimer": "Erscheint auf Ihren Rechnungen. Bank-/IBAN- und Mehrwertsteuerangaben liegen in Ihrer Verantwortung — prüfen Sie sie mit Ihrer Bank und Ihrem Treuhänder/Steuerberater.",
+ "companyName": "Firma / rechtlicher Name",
+ "addressLine1": "Strasse und Nummer",
+ "postalCode": "PLZ",
+ "city": "Ort",
+ "countryCode": "Ländercode (z. B. CH)",
+ "currency": "Währung (z. B. CHF)",
+ "vatId": "MwSt-Nummer (oder leer lassen)",
+ "taxId": "Steuernummer (oder MwSt-Nummer)",
+ "iban": "IBAN (für Rechnungszahlungen)",
+ "email": "E-Mail-Versand (SMTP)",
+ "emailHint": "Erforderlich, um Erinnerungen, Rechnungen und Benachrichtigungen zu senden.",
+ "smtpHost": "SMTP-Host",
+ "smtpPort": "Port",
+ "smtpUser": "Benutzername",
+ "smtpPass": "Passwort",
+ "fromEmail": "Absenderadresse",
+ "fromName": "Absendername",
+ "skip": "Vorerst überspringen",
+ "finish": "Einrichtung abschließen",
+ "saveFailed": "Einige Einstellungen konnten nicht gespeichert werden — Sie können sie in den Einstellungen abschließen."
+ },
"stepOf": "Schritt {{current}} von {{total}}",
"continue": "Weiter",
"back": "Zurück",
diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json
index d2e437e5..3ba9cb3c 100644
--- a/frontend/src/i18n/locales/en.json
+++ b/frontend/src/i18n/locales/en.json
@@ -3405,6 +3405,32 @@
"restoreEntry": "Migrating from another PicPeak?",
"restoreEntryHint": "Restore a .picpeak backup instead of setting up fresh.",
"restoreIntro": "Upload a .picpeak backup to clone another instance onto this one. This replaces everything except the account you just created.",
+ "config": {
+ "subtitle": "Set up your features",
+ "intro": "A few details for the features you picked. Anything you skip keeps its default and can be set later in Settings.",
+ "invoicing": "Invoicing details",
+ "invoicingDisclaimer": "Used on your invoices. Bank/IBAN and VAT details are your responsibility — verify them with your bank and Treuhänder/tax advisor.",
+ "companyName": "Company / legal name",
+ "addressLine1": "Street and number",
+ "postalCode": "Postal code",
+ "city": "City",
+ "countryCode": "Country code (e.g. CH)",
+ "currency": "Currency (e.g. CHF)",
+ "vatId": "VAT ID (or leave blank)",
+ "taxId": "Tax number (or VAT ID)",
+ "iban": "IBAN (for invoice payments)",
+ "email": "Email delivery (SMTP)",
+ "emailHint": "Required to send reminders, invoices and notifications.",
+ "smtpHost": "SMTP host",
+ "smtpPort": "Port",
+ "smtpUser": "Username",
+ "smtpPass": "Password",
+ "fromEmail": "From address",
+ "fromName": "From name",
+ "skip": "Skip for now",
+ "finish": "Finish setup",
+ "saveFailed": "Some settings could not be saved — you can finish them in Settings."
+ },
"stepOf": "Step {{current}} of {{total}}",
"continue": "Continue",
"back": "Back",
diff --git a/frontend/src/pages/SetupPage.tsx b/frontend/src/pages/SetupPage.tsx
index 48fa2f0a..790c01e7 100644
--- a/frontend/src/pages/SetupPage.tsx
+++ b/frontend/src/pages/SetupPage.tsx
@@ -10,6 +10,7 @@ import { useAdminAuth } from '../contexts';
import { setupService } from '../services/setup.service';
import { featureFlagsService, type FeatureFlags, type FeatureKey } from '../services/featureFlags.service';
import { PicpeakRestoreCard } from '../components/admin/PicpeakBackupCard';
+import { SetupConfigStep } from '../components/admin/SetupConfigStep';
import { resolveLoginLogoClasses } from '../utils/loginLogoSize';
import type { AdminUser } from '../types';
@@ -52,7 +53,7 @@ export const SetupPage: React.FC = () => {
staleTime: Infinity,
});
- const [step, setStep] = useState<'token' | 'account' | 'usage' | 'restore'>('token');
+ const [step, setStep] = useState<'token' | 'account' | 'usage' | 'restore' | 'config'>('token');
const [form, setForm] = useState({ token: '', email: '', password: '', confirm: '' });
const [showPassword, setShowPassword] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -226,7 +227,15 @@ export const SetupPage: React.FC = () => {
toast.warn(t('setup.featuresSaveFailed'));
} finally {
setIsSavingFeatures(false);
- navigate('/admin/dashboard', { replace: true });
+ // If the chosen features need config the wizard can collect (invoicing,
+ // email), go to the config step; otherwise enter the app.
+ const needsConfig =
+ selectedFeatures.has('bills') ||
+ selectedFeatures.has('reminderEmails') ||
+ selectedFeatures.has('incomingMail') ||
+ selectedFeatures.has('whatsapp');
+ if (needsConfig) setStep('config');
+ else navigate('/admin/dashboard', { replace: true });
}
};
@@ -256,9 +265,11 @@ export const SetupPage: React.FC = () => {
? t('setup.accountStepSubtitle')
: step === 'restore'
? t('setup.restoreStepSubtitle')
- : t('setup.usageSubtitle')}
+ : step === 'config'
+ ? t('setup.config.subtitle')
+ : t('setup.usageSubtitle')}
- {step !== 'restore' && (
+ {(step === 'token' || step === 'account' || step === 'usage') && (
{t('setup.stepOf', { current: stepNumber, total: 3 })}
@@ -461,7 +472,7 @@ export const SetupPage: React.FC = () => {
{selectedFeatures.size > 0 ? t('setup.finish') : t('setup.usageSkip')}
- ) : (
+ ) : step === 'restore' ? (
{t('setup.restoreIntro')}
@@ -475,6 +486,11 @@ export const SetupPage: React.FC = () => {
{t('setup.back')}
+ ) : (
+ navigate('/admin/dashboard', { replace: true })}
+ />
)}