From 48c63a896f62c3bbd99779aaf36c3aa60181f77b Mon Sep 17 00:00:00 2001 From: seaznCode Date: Mon, 9 Mar 2026 22:07:28 +0100 Subject: [PATCH] feat: add company settings management and guest registration functionality --- .../components/companySettingsPanel.tsx | 140 +++++++++ .../hooks/useContractManagement.ts | 18 ++ src/app/admin/contract-management/page.tsx | 10 + src/app/register/components/RegisterForm.tsx | 274 ++++++++++++++++-- src/app/register/hooks/useRegister.ts | 67 +++++ src/app/register/page.tsx | 19 +- 6 files changed, 495 insertions(+), 33 deletions(-) create mode 100644 src/app/admin/contract-management/components/companySettingsPanel.tsx diff --git a/src/app/admin/contract-management/components/companySettingsPanel.tsx b/src/app/admin/contract-management/components/companySettingsPanel.tsx new file mode 100644 index 0000000..fa6e628 --- /dev/null +++ b/src/app/admin/contract-management/components/companySettingsPanel.tsx @@ -0,0 +1,140 @@ +'use client' + +import { useState, useEffect } from 'react' +import useContractManagement from '../hooks/useContractManagement' + +export default function CompanySettingsPanel() { + const { getCompanySettings, updateCompanySettings } = useContractManagement() + + const [form, setForm] = useState({ + company_name: '', + company_street: '', + company_postal_city: '', + company_country: '', + }) + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + const [saved, setSaved] = useState(false) + + useEffect(() => { + getCompanySettings() + .then(data => { + setForm({ + company_name: data.company_name || '', + company_street: data.company_street || '', + company_postal_city: data.company_postal_city || '', + company_country: data.company_country || '', + }) + }) + .catch(() => {}) + .finally(() => setLoading(false)) + }, [getCompanySettings]) + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + setForm(prev => ({ ...prev, [name]: value })) + setSaved(false) + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setSaving(true) + setSaved(false) + try { + await updateCompanySettings(form) + setSaved(true) + setTimeout(() => setSaved(false), 3000) + } catch { + // silent + } finally { + setSaving(false) + } + } + + if (loading) { + return ( +
+
+ Loading settings… +
+ ) + } + + return ( +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + {saved && ( + Saved successfully + )} +
+
+ ) +} diff --git a/src/app/admin/contract-management/hooks/useContractManagement.ts b/src/app/admin/contract-management/hooks/useContractManagement.ts index 431729f..c1f239a 100644 --- a/src/app/admin/contract-management/hooks/useContractManagement.ts +++ b/src/app/admin/contract-management/hooks/useContractManagement.ts @@ -444,6 +444,21 @@ export default function useContractManagement() { return Array.isArray(data) ? data : []; }, [authorizedFetch]); + // Company settings (invoice address info) + const getCompanySettings = useCallback(async () => { + return authorizedFetch<{ company_name: string; company_street: string; company_postal_city: string; company_country: string }>( + '/api/admin/company-settings', { method: 'GET' } + ); + }, [authorizedFetch]); + + const updateCompanySettings = useCallback(async (data: { + company_name: string; company_street: string; company_postal_city: string; company_country: string; + }) => { + return authorizedFetch<{ company_name: string; company_street: string; company_postal_city: string; company_country: string }>( + '/api/admin/company-settings', { method: 'PUT', body: JSON.stringify(data) } + ); + }, [authorizedFetch]); + return { // templates listTemplates, @@ -466,6 +481,9 @@ export default function useContractManagement() { activateCompanyStamp, deactivateCompanyStamp, deleteCompanyStamp, + // company settings + getCompanySettings, + updateCompanySettings, // utils downloadBlobFile, }; diff --git a/src/app/admin/contract-management/page.tsx b/src/app/admin/contract-management/page.tsx index b0caeb5..7f4f7bb 100644 --- a/src/app/admin/contract-management/page.tsx +++ b/src/app/admin/contract-management/page.tsx @@ -4,6 +4,7 @@ import React, { useState, useEffect } from 'react'; import PageLayout from '../../components/PageLayout'; import ContractEditor from './components/contractEditor'; import ContractUploadCompanyStamp from './components/contractUploadCompanyStamp'; +import CompanySettingsPanel from './components/companySettingsPanel'; import ContractTemplateList from './components/contractTemplateList'; import useAuthStore from '../../store/authStore'; import { useRouter } from 'next/navigation'; @@ -98,6 +99,15 @@ export default function ContractManagementPage() { Company Stamp + +
+

+ + Company Information +

+

Address details used on invoices.

+ +
)} {section === 'templates' && ( diff --git a/src/app/register/components/RegisterForm.tsx b/src/app/register/components/RegisterForm.tsx index a59b7e6..b36c153 100644 --- a/src/app/register/components/RegisterForm.tsx +++ b/src/app/register/components/RegisterForm.tsx @@ -7,8 +7,8 @@ import { useToast } from '../../components/toast/toastComponent' import TelephoneInput, { TelephoneInputHandle } from '../../components/phone/telephoneInput' interface RegisterFormProps { - mode: 'personal' | 'company' - setMode: (mode: 'personal' | 'company') => void + mode: 'personal' | 'company' | 'guest' + setMode: (mode: 'personal' | 'company' | 'guest') => void refToken: string | null onRegistered: () => void referrerEmail?: string @@ -78,9 +78,19 @@ export default function RegisterForm({ const contactPhoneRef = useRef(null) // Hook for backend calls - const { registerPersonalReferral, registerCompanyReferral, error: regError } = useRegister() + const { registerPersonalReferral, registerCompanyReferral, registerGuest, error: regError } = useRegister() const { showToast } = useToast() + // Guest form state + const [guestForm, setGuestForm] = useState({ + firstName: '', + lastName: '', + email: '', + confirmEmail: '', + password: '', + confirmPassword: '', + }) + // Animate form when mode changes useEffect(() => { setFormFade('fade-out') @@ -350,7 +360,72 @@ export default function RegisterForm({ const { name, value } = e.target setCompanyForm(prev => ({ ...prev, [name]: value })) } - + + const handleGuestChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + setGuestForm(prev => ({ ...prev, [name]: value })) + } + + const validateGuestForm = (): boolean => { + if (!guestForm.firstName.trim() || !guestForm.lastName.trim() || + !guestForm.email.trim() || !guestForm.confirmEmail.trim() || + !guestForm.password.trim() || !guestForm.confirmPassword.trim() + ) { + setError('All fields are required') + return false + } + if (guestForm.email !== guestForm.confirmEmail) { + setError('Email addresses do not match') + return false + } + if (guestForm.password !== guestForm.confirmPassword) { + setError('Passwords do not match') + return false + } + if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,}$/.test(guestForm.password)) { + setError('Password must be at least 8 characters long and contain uppercase and lowercase letters, numbers and special characters') + return false + } + setError('') + return true + } + + const handleGuestSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (loading) return + if (!validateGuestForm()) return + + setLoading(true) + setError('') + try { + const res = await registerGuest({ + refToken: refToken || '', + firstName: guestForm.firstName, + lastName: guestForm.lastName, + email: guestForm.email, + password: guestForm.password, + }) + if (res.ok) { + showToast({ + variant: 'success', + title: 'Registration successful', + message: 'You can now log in to view your coffee abonnement.' + }) + onRegistered() + } else { + const msg = res.message || 'Registration failed. Please try again.' + setError(msg) + showToast({ variant: 'error', title: 'Registration failed', message: msg }) + } + } catch { + const msg = 'Registration failed. Please try again.' + setError(msg) + showToast({ variant: 'error', title: 'Registration failed', message: msg }) + } finally { + setLoading(false) + } + } + // Password strength indicator const getPasswordStrength = (password: string) => { let strength = 0 @@ -408,28 +483,39 @@ export default function RegisterForm({ {/* Mode Toggle */}
- - + {mode === 'guest' ? ( + + ) : ( + <> + + + + )}
@@ -592,7 +678,7 @@ export default function RegisterForm({ )} - ) : ( + ) : mode === 'company' ? (
@@ -764,6 +850,140 @@ export default function RegisterForm({ )} + ) : ( +
+
+

+ You are registering as a guest. You will have access to your coffee abonnements only. +

+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ +
+ + +
+ {guestForm.password && renderPasswordStrength(guestForm.password)} +
+
+ + +
+
+ + +
)}
diff --git a/src/app/register/hooks/useRegister.ts b/src/app/register/hooks/useRegister.ts index 8a37782..3245b43 100644 --- a/src/app/register/hooks/useRegister.ts +++ b/src/app/register/hooks/useRegister.ts @@ -23,6 +23,15 @@ export type CompanyReferralPayload = { lang?: 'de' | 'en' } +export type GuestReferralPayload = { + refToken: string + firstName: string + lastName: string + email: string + password: string + lang?: 'de' | 'en' +} + type RegisterResult = { ok: boolean status: number @@ -144,11 +153,69 @@ export function useRegister() { } } + const registerGuest = async (payload: GuestReferralPayload): Promise => { + setError('') + setLoading(true) + + const body = { + firstName: payload.firstName, + lastName: payload.lastName, + email: payload.email, + confirmEmail: payload.email, + password: payload.password, + confirmPassword: payload.password, + referralEmail: undefined as string | undefined, + lang: payload.lang || detectLang(), + } + + // Try to resolve referral email from token + if (payload.refToken) { + try { + const infoRes = await fetch(`${base}/api/referral/info/${encodeURIComponent(payload.refToken)}`, { + method: 'GET', + credentials: 'include', + }) + const infoJson = await infoRes.json().catch(() => null) + if (infoJson?.referrerEmail) { + body.referralEmail = infoJson.referrerEmail + } + } catch {} + } + + const url = `${base}/api/register/guest` + console.log('🌐 useRegister: POST guest', { url }) + + try { + const res = await fetch(url, { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) + const json = await res.json().catch(() => null) + console.log('📡 useRegister: guest status:', res.status, 'body:', json) + if (!res.ok) { + const msg = json?.message || mapError(res.status) + setError(msg) + return { ok: false, status: res.status, data: json, message: msg } + } + return { ok: true, status: res.status, data: json, message: json?.message } + } catch (e) { + console.error('❌ useRegister: guest error:', e) + const msg = 'Network error. Please try again later.' + setError(msg) + return { ok: false, status: 0, data: null, message: msg } + } finally { + setLoading(false) + } + } + return { loading, error, setError, registerPersonalReferral, registerCompanyReferral, + registerGuest, } } diff --git a/src/app/register/page.tsx b/src/app/register/page.tsx index 72458ab..a7dcca8 100644 --- a/src/app/register/page.tsx +++ b/src/app/register/page.tsx @@ -15,8 +15,9 @@ import BlueBlurryBackground from '../components/background/blueblurry' // NEW function RegisterPageInner() { const searchParams = useSearchParams() const refToken = searchParams.get('ref') + const isGuestInvite = searchParams.get('guest') === 'true' const [registered, setRegistered] = useState(false) - const [mode, setMode] = useState<'personal' | 'company'>('personal') + const [mode, setMode] = useState<'personal' | 'company' | 'guest'>(isGuestInvite ? 'guest' : 'personal') const router = useRouter() const { showToast } = useToast() @@ -71,6 +72,8 @@ function RegisterPageInner() { const res = await fetch(url, { method: 'GET', credentials: 'include' }) const body = await res.json().catch(() => null) + console.log('[REGISTER] Token validation response:', { status: res.status, body }) + const success = !!body?.success const isUnlimited = !!body?.isUnlimited const usesRemaining = @@ -92,16 +95,18 @@ function RegisterPageInner() { message: 'Your invitation link is valid. You can register now.' }) } else { + const reason = body?.reason || `HTTP ${res.status}` setInvalidRef(true) showToast({ variant: 'error', title: 'Invalid invitation', - message: 'This invitation link is invalid or no longer active.' + message: `Reason: ${reason}. This invitation link is invalid or no longer active.` }) } setIsRefChecked(true) } - } catch { + } catch (err) { + console.error('[REGISTER] Token validation network error:', err) if (!cancelled) { setInvalidRef(true) setIsRefChecked(true) @@ -109,7 +114,7 @@ function RegisterPageInner() { showToast({ variant: 'error', title: 'Network error', - message: 'Could not validate the invitation link. Please try again.' + message: 'Could not reach the server. Is the backend running?' }) } } @@ -277,10 +282,12 @@ function RegisterPageInner() {

- Register now + {mode === 'guest' ? 'Guest Registration' : 'Register now'}

- Create your personal or company account with Profit Planet. + {mode === 'guest' + ? 'Register as a guest to access your coffee abonnement.' + : 'Create your personal or company account with Profit Planet.'}