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 && ( +
+

{t('setup.config.invoicing', 'Invoicing details')}

+
+ +

+ {t('setup.config.invoicingDisclaimer', 'Used on your invoices. Bank/IBAN and VAT details are your responsibility — verify them with your bank and Treuhänder/tax advisor.')} +

+
+ + +
+ +
+
+
+ + +
+
+ + +
+ +
+ )} + + {showEmail && ( +
+

{t('setup.config.email', 'Email delivery (SMTP)')}

+

{t('setup.config.emailHint', 'Required to send reminders, invoices and notifications.')}

+
+
+ +
+
+ + +
+
+ + +
+
+ )} + +
+ + +
+
+ ); +}; + +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 })} + /> )}