lomavuokraus/app/me/page.tsx
Tero Halla-aho 49a1096152
Some checks are pending
CI / checks (push) Waiting to run
Add billing agent opt-in settings and per-listing overrides
2025-12-13 22:23:47 +02:00

288 lines
12 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import { useI18n } from '../components/I18nProvider';
type User = { id: string; email: string; role: string; status: string; emailVerifiedAt: string | null; approvedAt: string | null; name: string | null; phone: string | null };
type ListingOverride = { id: string; label: string; bankAccount: string; vatBreakdownRequired: boolean };
type BillingSettings = {
agentBillingEnabled: boolean;
agentBankAccount: string;
agentVatBreakdownRequired: boolean;
agentUseListingOverrides: boolean;
listingOverrides: ListingOverride[];
};
export default function ProfilePage() {
const { t } = useI18n();
const [user, setUser] = useState<User | null>(null);
const [error, setError] = useState<string | null>(null);
const [name, setName] = useState('');
const [phone, setPhone] = useState('');
const [password, setPassword] = useState('');
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState<string | null>(null);
const [billingSettings, setBillingSettings] = useState<BillingSettings | null>(null);
const [billingError, setBillingError] = useState<string | null>(null);
const [billingMessage, setBillingMessage] = useState<string | null>(null);
const [billingSaving, setBillingSaving] = useState(false);
useEffect(() => {
const load = async () => {
try {
const res = await fetch('/api/auth/me', { cache: 'no-store' });
const data = await res.json();
if (data.user) {
setUser(data.user);
setName(data.user.name ?? '');
setPhone(data.user.phone ?? '');
const billingRes = await fetch('/api/billing-settings', { cache: 'no-store' });
const billingData = await billingRes.json();
if (billingRes.ok && billingData.settings) {
setBillingSettings({
agentBillingEnabled: billingData.settings.agentBillingEnabled,
agentBankAccount: billingData.settings.agentBankAccount ?? '',
agentVatBreakdownRequired: billingData.settings.agentVatBreakdownRequired,
agentUseListingOverrides: billingData.settings.agentUseListingOverrides,
listingOverrides: billingData.listingOverrides ?? [],
});
} else {
setBillingError(billingData.error || 'Failed to load billing settings');
}
} else {
setError(t('notLoggedIn'));
}
} catch (err) {
setError(t('notLoggedIn'));
}
};
load();
}, [t]);
async function onSave(e: React.FormEvent) {
e.preventDefault();
setSaving(true);
setError(null);
setMessage(null);
try {
const res = await fetch('/api/me', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, phone, password: password || undefined }),
});
const data = await res.json();
if (!res.ok) {
setError(data.error || 'Update failed');
} else {
setUser(data.user);
setPassword('');
setMessage(t('profileUpdated'));
}
} catch (err) {
setError('Update failed');
} finally {
setSaving(false);
}
}
async function onSaveBilling(e: React.FormEvent) {
e.preventDefault();
if (!billingSettings) return;
setBillingSaving(true);
setBillingError(null);
setBillingMessage(null);
try {
const res = await fetch('/api/billing-settings', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentBillingEnabled: billingSettings.agentBillingEnabled,
agentBankAccount: billingSettings.agentBankAccount,
agentVatBreakdownRequired: billingSettings.agentVatBreakdownRequired,
agentUseListingOverrides: billingSettings.agentUseListingOverrides,
listingOverrides: billingSettings.listingOverrides.map((o) => ({
listingId: o.id,
bankAccount: o.bankAccount,
vatBreakdownRequired: o.vatBreakdownRequired,
})),
}),
});
const data = await res.json();
if (!res.ok) {
setBillingError(data.error || 'Update failed');
} else {
setBillingMessage(t('billingSettingsSaved'));
}
} catch (err) {
setBillingError('Update failed');
} finally {
setBillingSaving(false);
}
}
return (
<main className="panel" style={{ maxWidth: 640, margin: '40px auto' }}>
<h1>{t('myProfileTitle')}</h1>
{message ? <p style={{ color: 'green' }}>{message}</p> : null}
{user ? (
<>
<ul>
<li>
<strong>{t('profileEmail')}:</strong> {user.email}
</li>
<li>
<strong>{t('profileName')}:</strong> {user.name ?? '—'}
</li>
<li>
<strong>{t('profilePhone')}:</strong> {user.phone ?? '—'}
</li>
<li>
<strong>{t('profileRole')}:</strong> {user.role}
</li>
<li>
<strong>{t('profileStatus')}:</strong> {user.status}
</li>
<li>
<strong>{t('profileEmailVerified')}:</strong> {user.emailVerifiedAt ? t('yes') : t('no')}
</li>
<li>
<strong>{t('profileApproved')}:</strong> {user.approvedAt ? t('yes') : t('no')}
</li>
</ul>
<div style={{ marginTop: 16, display: 'flex', gap: 10, flexWrap: 'wrap' }}>
<a className="button secondary" href="/listings/mine">
{t('navMyListings')}
</a>
<a className="button secondary" href="/listings/new">
{t('navNewListing')}
</a>
</div>
<form onSubmit={onSave} style={{ marginTop: 20, display: 'grid', gap: 10, maxWidth: 420 }}>
<label>
{t('profileName')}
<input value={name} onChange={(e) => setName(e.target.value)} />
</label>
<label>
{t('profilePhone')}
<input value={phone} onChange={(e) => setPhone(e.target.value)} />
</label>
<label>
{t('passwordLabel')} ({t('passwordHint')})
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} minLength={8} />
</label>
<p style={{ fontSize: 12, color: '#666' }}>{t('emailLocked')}</p>
<button className="button" type="submit" disabled={saving}>
{saving ? t('saving') : t('save')}
</button>
</form>
<section style={{ marginTop: 32, paddingTop: 20, borderTop: '1px solid #e5e7eb' }}>
<h2 style={{ marginBottom: 12 }}>{t('billingAgentTitle')}</h2>
{billingMessage ? <p style={{ color: 'green' }}>{billingMessage}</p> : null}
{billingError ? <p style={{ color: 'red' }}>{billingError}</p> : null}
{billingSettings ? (
<form onSubmit={onSaveBilling} style={{ display: 'grid', gap: 12 }}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<input
type="checkbox"
checked={billingSettings.agentBillingEnabled}
onChange={(e) =>
setBillingSettings({ ...billingSettings, agentBillingEnabled: e.target.checked })
}
/>
<span>{t('billingAgentOptIn')}</span>
</label>
<p style={{ margin: 0, color: '#4b5563', fontSize: 14 }}>{t('billingAgentOptInHelp')}</p>
<label style={{ opacity: billingSettings.agentBillingEnabled ? 1 : 0.6 }}>
{t('billingDefaultBank')}
<input
value={billingSettings.agentBankAccount}
onChange={(e) =>
setBillingSettings({ ...billingSettings, agentBankAccount: e.target.value })
}
placeholder="FI.. / payment instructions"
disabled={!billingSettings.agentBillingEnabled}
/>
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: 8, opacity: billingSettings.agentBillingEnabled ? 1 : 0.6 }}>
<input
type="checkbox"
checked={billingSettings.agentVatBreakdownRequired}
onChange={(e) =>
setBillingSettings({ ...billingSettings, agentVatBreakdownRequired: e.target.checked })
}
disabled={!billingSettings.agentBillingEnabled}
/>
<span>{t('billingVatBreakdown')}</span>
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: 8, opacity: billingSettings.agentBillingEnabled ? 1 : 0.6 }}>
<input
type="checkbox"
checked={billingSettings.agentUseListingOverrides}
onChange={(e) =>
setBillingSettings({ ...billingSettings, agentUseListingOverrides: e.target.checked })
}
disabled={!billingSettings.agentBillingEnabled}
/>
<span>{t('billingUseListingOverrides')}</span>
</label>
{billingSettings.agentBillingEnabled && billingSettings.agentUseListingOverrides ? (
<div style={{ display: 'grid', gap: 10 }}>
{billingSettings.listingOverrides.length === 0 ? (
<p style={{ color: '#4b5563', margin: 0 }}>{t('billingNoListings')}</p>
) : (
billingSettings.listingOverrides.map((listing) => (
<div
key={listing.id}
style={{ border: '1px solid #e5e7eb', borderRadius: 6, padding: 12, display: 'grid', gap: 8 }}
>
<div style={{ fontWeight: 600 }}>{listing.label}</div>
<label>
{t('billingListingBank')}
<input
value={listing.bankAccount}
onChange={(e) =>
setBillingSettings({
...billingSettings,
listingOverrides: billingSettings.listingOverrides.map((l) =>
l.id === listing.id ? { ...l, bankAccount: e.target.value } : l
),
})
}
placeholder={t('billingListingBankPlaceholder')}
/>
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<input
type="checkbox"
checked={listing.vatBreakdownRequired}
onChange={(e) =>
setBillingSettings({
...billingSettings,
listingOverrides: billingSettings.listingOverrides.map((l) =>
l.id === listing.id ? { ...l, vatBreakdownRequired: e.target.checked } : l
),
})
}
/>
<span>{t('billingListingVat')}</span>
</label>
</div>
))
)}
</div>
) : null}
<button className="button" type="submit" disabled={billingSaving}>
{billingSaving ? t('saving') : t('save')}
</button>
</form>
) : (
<p style={{ color: '#4b5563' }}>{t('loading')}</p>
)}
</section>
</>
) : (
<p style={{ color: 'red' }}>{error ?? t('notLoggedIn')}</p>
)}
</main>
);
}