Add tenant admin account edit page
This commit is contained in:
@@ -13,6 +13,7 @@ export const ADMIN_AUTH_CALLBACK_PATH = adminPath('/auth/callback');
|
||||
export const ADMIN_EVENTS_PATH = adminPath('/mobile/events');
|
||||
export const ADMIN_SETTINGS_PATH = adminPath('/mobile/settings');
|
||||
export const ADMIN_PROFILE_PATH = adminPath('/mobile/profile');
|
||||
export const ADMIN_PROFILE_ACCOUNT_PATH = adminPath('/mobile/profile/account');
|
||||
export const ADMIN_FAQ_PATH = adminPath('/mobile/help');
|
||||
export const ADMIN_BILLING_PATH = adminPath('/mobile/billing');
|
||||
export const ADMIN_PACKAGE_SHOP_PATH = adminPath('/mobile/billing/shop');
|
||||
|
||||
@@ -2392,7 +2392,7 @@
|
||||
"mobileProfile": {
|
||||
"title": "Profil",
|
||||
"settings": "Einstellungen",
|
||||
"account": "Account & Sicherheit",
|
||||
"account": "Account bearbeiten",
|
||||
"language": "Sprache",
|
||||
"languageDe": "Deutsch",
|
||||
"languageEn": "Englisch",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"profile": {
|
||||
"title": "Profil",
|
||||
"subtitle": "Verwalte deine Kontodaten und Zugangsdaten.",
|
||||
"loading": "Lädt ...",
|
||||
"sections": {
|
||||
"account": {
|
||||
"heading": "Account-Informationen",
|
||||
|
||||
@@ -2396,7 +2396,7 @@
|
||||
"mobileProfile": {
|
||||
"title": "Profile",
|
||||
"settings": "Settings",
|
||||
"account": "Account & security",
|
||||
"account": "Edit account",
|
||||
"language": "Language",
|
||||
"languageDe": "Deutsch",
|
||||
"languageEn": "English",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"profile": {
|
||||
"title": "Profile",
|
||||
"subtitle": "Manage your account details and credentials.",
|
||||
"loading": "Loading ...",
|
||||
"sections": {
|
||||
"account": {
|
||||
"heading": "Account information",
|
||||
|
||||
302
resources/js/admin/mobile/ProfileAccountPage.tsx
Normal file
302
resources/js/admin/mobile/ProfileAccountPage.tsx
Normal file
@@ -0,0 +1,302 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CheckCircle2, Lock, MailWarning, User } from 'lucide-react';
|
||||
import { YStack, XStack } from '@tamagui/stacks';
|
||||
import { SizableText as Text } from '@tamagui/text';
|
||||
import toast from 'react-hot-toast';
|
||||
import { MobileShell } from './components/MobileShell';
|
||||
import { MobileCard, CTAButton, PillBadge } from './components/Primitives';
|
||||
import { MobileField, MobileInput, MobileSelect } from './components/FormControls';
|
||||
import { fetchTenantProfile, updateTenantProfile, type TenantAccountProfile } from '../api';
|
||||
import { getApiErrorMessage, getApiValidationMessage } from '../lib/apiError';
|
||||
import { ADMIN_PROFILE_PATH } from '../constants';
|
||||
import { useBackNavigation } from './hooks/useBackNavigation';
|
||||
import { useAdminTheme } from './theme';
|
||||
import i18n from '../i18n';
|
||||
|
||||
type ProfileFormState = {
|
||||
name: string;
|
||||
email: string;
|
||||
preferredLocale: string;
|
||||
currentPassword: string;
|
||||
password: string;
|
||||
passwordConfirmation: string;
|
||||
};
|
||||
|
||||
const LOCALE_OPTIONS = [
|
||||
{ value: '', labelKey: 'profile.locale.auto', fallback: 'Automatisch' },
|
||||
{ value: 'de', label: 'Deutsch' },
|
||||
{ value: 'en', label: 'English' },
|
||||
];
|
||||
|
||||
export default function MobileProfileAccountPage() {
|
||||
const { t } = useTranslation('settings');
|
||||
const { text, muted, danger, subtle, primary, accentSoft } = useAdminTheme();
|
||||
const back = useBackNavigation(ADMIN_PROFILE_PATH);
|
||||
|
||||
const [profile, setProfile] = React.useState<TenantAccountProfile | null>(null);
|
||||
const [form, setForm] = React.useState<ProfileFormState>({
|
||||
name: '',
|
||||
email: '',
|
||||
preferredLocale: '',
|
||||
currentPassword: '',
|
||||
password: '',
|
||||
passwordConfirmation: '',
|
||||
});
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [savingAccount, setSavingAccount] = React.useState(false);
|
||||
const [savingPassword, setSavingPassword] = React.useState(false);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const loadErrorMessage = t('profile.errors.load', 'Profil konnte nicht geladen werden.');
|
||||
|
||||
const dateFormatter = React.useMemo(
|
||||
() =>
|
||||
new Intl.DateTimeFormat(i18n.language || 'de', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
}),
|
||||
[i18n.language],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await fetchTenantProfile();
|
||||
setProfile(data);
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
name: data.name ?? '',
|
||||
email: data.email ?? '',
|
||||
preferredLocale: data.preferred_locale ?? '',
|
||||
}));
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(getApiErrorMessage(err, loadErrorMessage));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const verifiedAt = profile?.email_verified_at ? new Date(profile.email_verified_at) : null;
|
||||
const verifiedDate = verifiedAt ? dateFormatter.format(verifiedAt) : null;
|
||||
const emailStatusLabel = profile?.email_verified
|
||||
? t('profile.status.emailVerified', 'E-Mail bestätigt')
|
||||
: t('profile.status.emailNotVerified', 'Bestätigung erforderlich');
|
||||
const emailHint = profile?.email_verified
|
||||
? t('profile.status.verifiedHint', 'Bestätigt am {{date}}.', { date: verifiedDate ?? '' })
|
||||
: t('profile.status.unverifiedHint', 'Bei Änderung der E-Mail senden wir dir automatisch eine neue Bestätigung.');
|
||||
|
||||
const buildPayload = (includePassword: boolean) => ({
|
||||
name: form.name.trim(),
|
||||
email: form.email.trim(),
|
||||
preferred_locale: form.preferredLocale ? form.preferredLocale : null,
|
||||
...(includePassword
|
||||
? {
|
||||
current_password: form.currentPassword,
|
||||
password: form.password,
|
||||
password_confirmation: form.passwordConfirmation,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
|
||||
const handleAccountSave = async () => {
|
||||
setSavingAccount(true);
|
||||
try {
|
||||
const updated = await updateTenantProfile(buildPayload(false));
|
||||
setProfile(updated);
|
||||
setError(null);
|
||||
toast.success(t('profile.toasts.updated', 'Profil wurde aktualisiert.'));
|
||||
} catch (err) {
|
||||
const message = getApiValidationMessage(err, t('profile.errors.update', 'Profil konnte nicht aktualisiert werden.'));
|
||||
setError(message);
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setSavingAccount(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasswordSave = async () => {
|
||||
setSavingPassword(true);
|
||||
try {
|
||||
const updated = await updateTenantProfile(buildPayload(true));
|
||||
setProfile(updated);
|
||||
setError(null);
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
currentPassword: '',
|
||||
password: '',
|
||||
passwordConfirmation: '',
|
||||
}));
|
||||
toast.success(t('profile.toasts.passwordChanged', 'Passwort wurde aktualisiert.'));
|
||||
} catch (err) {
|
||||
const message = getApiValidationMessage(err, t('profile.errors.update', 'Profil konnte nicht aktualisiert werden.'));
|
||||
setError(message);
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setSavingPassword(false);
|
||||
}
|
||||
};
|
||||
|
||||
const passwordReady =
|
||||
form.currentPassword.trim().length > 0 &&
|
||||
form.password.trim().length > 0 &&
|
||||
form.passwordConfirmation.trim().length > 0;
|
||||
|
||||
return (
|
||||
<MobileShell
|
||||
activeTab="profile"
|
||||
title={t('profile.title', 'Profil')}
|
||||
onBack={back}
|
||||
>
|
||||
{error ? (
|
||||
<MobileCard>
|
||||
<Text fontWeight="700" color={danger}>
|
||||
{error}
|
||||
</Text>
|
||||
</MobileCard>
|
||||
) : null}
|
||||
|
||||
<MobileCard space="$3">
|
||||
<XStack alignItems="center" space="$3">
|
||||
<XStack
|
||||
width={48}
|
||||
height={48}
|
||||
borderRadius={16}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
backgroundColor={accentSoft}
|
||||
>
|
||||
<User size={20} color={primary} />
|
||||
</XStack>
|
||||
<YStack space="$1">
|
||||
<Text fontSize="$md" fontWeight="800" color={text}>
|
||||
{form.name || profile?.email || t('profile.title', 'Profil')}
|
||||
</Text>
|
||||
<Text fontSize="$sm" color={muted}>
|
||||
{form.email || profile?.email || '—'}
|
||||
</Text>
|
||||
</YStack>
|
||||
</XStack>
|
||||
<XStack alignItems="center" space="$2" flexWrap="wrap">
|
||||
{profile?.email_verified ? (
|
||||
<CheckCircle2 size={14} color={subtle} />
|
||||
) : (
|
||||
<MailWarning size={14} color={subtle} />
|
||||
)}
|
||||
<PillBadge tone={profile?.email_verified ? 'success' : 'warning'}>
|
||||
{emailStatusLabel}
|
||||
</PillBadge>
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{emailHint}
|
||||
</Text>
|
||||
</XStack>
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<User size={16} color={text} />
|
||||
<Text fontSize="$md" fontWeight="800" color={text}>
|
||||
{t('profile.sections.account.heading', 'Account-Informationen')}
|
||||
</Text>
|
||||
</XStack>
|
||||
<Text fontSize="$sm" color={muted}>
|
||||
{t('profile.sections.account.description', 'Passe Anzeigename, E-Mail-Adresse und Sprache der Admin-Oberfläche an.')}
|
||||
</Text>
|
||||
{loading ? (
|
||||
<Text fontSize="$sm" color={muted}>
|
||||
{t('profile.loading', 'Lädt ...')}
|
||||
</Text>
|
||||
) : (
|
||||
<YStack space="$3">
|
||||
<MobileField label={t('profile.fields.name', 'Anzeigename')}>
|
||||
<MobileInput
|
||||
value={form.name}
|
||||
onChange={(event) => setForm((prev) => ({ ...prev, name: event.target.value }))}
|
||||
placeholder={t('profile.placeholders.name', 'z. B. Hochzeitsplanung Schmidt')}
|
||||
hasError={false}
|
||||
/>
|
||||
</MobileField>
|
||||
<MobileField label={t('profile.fields.email', 'E-Mail-Adresse')}>
|
||||
<MobileInput
|
||||
value={form.email}
|
||||
onChange={(event) => setForm((prev) => ({ ...prev, email: event.target.value }))}
|
||||
placeholder="mail@beispiel.de"
|
||||
type="email"
|
||||
hasError={false}
|
||||
/>
|
||||
</MobileField>
|
||||
<MobileField label={t('profile.fields.locale', 'Bevorzugte Sprache')}>
|
||||
<MobileSelect
|
||||
value={form.preferredLocale}
|
||||
onChange={(event) => setForm((prev) => ({ ...prev, preferredLocale: event.target.value }))}
|
||||
>
|
||||
{LOCALE_OPTIONS.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label ?? t(option.labelKey, option.fallback)}
|
||||
</option>
|
||||
))}
|
||||
</MobileSelect>
|
||||
</MobileField>
|
||||
<CTAButton
|
||||
label={t('profile.actions.save', 'Speichern')}
|
||||
onPress={handleAccountSave}
|
||||
disabled={savingAccount || loading}
|
||||
loading={savingAccount}
|
||||
/>
|
||||
</YStack>
|
||||
)}
|
||||
</MobileCard>
|
||||
|
||||
<MobileCard space="$3">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<Lock size={16} color={text} />
|
||||
<Text fontSize="$md" fontWeight="800" color={text}>
|
||||
{t('profile.sections.password.heading', 'Passwort ändern')}
|
||||
</Text>
|
||||
</XStack>
|
||||
<Text fontSize="$sm" color={muted}>
|
||||
{t('profile.sections.password.description', 'Wähle ein sicheres Passwort, um deinen Admin-Zugang zu schützen.')}
|
||||
</Text>
|
||||
<YStack space="$3">
|
||||
<MobileField label={t('profile.fields.currentPassword', 'Aktuelles Passwort')}>
|
||||
<MobileInput
|
||||
value={form.currentPassword}
|
||||
onChange={(event) => setForm((prev) => ({ ...prev, currentPassword: event.target.value }))}
|
||||
placeholder="••••••••"
|
||||
type="password"
|
||||
hasError={false}
|
||||
/>
|
||||
</MobileField>
|
||||
<MobileField label={t('profile.fields.newPassword', 'Neues Passwort')} hint={t('profile.sections.password.hint', 'Nutze mindestens 8 Zeichen und kombiniere Buchstaben sowie Zahlen für mehr Sicherheit.')}>
|
||||
<MobileInput
|
||||
value={form.password}
|
||||
onChange={(event) => setForm((prev) => ({ ...prev, password: event.target.value }))}
|
||||
placeholder="••••••••"
|
||||
type="password"
|
||||
hasError={false}
|
||||
/>
|
||||
</MobileField>
|
||||
<MobileField label={t('profile.fields.passwordConfirmation', 'Passwort bestätigen')}>
|
||||
<MobileInput
|
||||
value={form.passwordConfirmation}
|
||||
onChange={(event) => setForm((prev) => ({ ...prev, passwordConfirmation: event.target.value }))}
|
||||
placeholder="••••••••"
|
||||
type="password"
|
||||
hasError={false}
|
||||
/>
|
||||
</MobileField>
|
||||
<CTAButton
|
||||
label={t('profile.actions.updatePassword', 'Passwort aktualisieren')}
|
||||
onPress={handlePasswordSave}
|
||||
disabled={!passwordReady || savingPassword || loading}
|
||||
loading={savingPassword}
|
||||
tone="ghost"
|
||||
/>
|
||||
</YStack>
|
||||
</MobileCard>
|
||||
</MobileShell>
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import { MobileCard, CTAButton } from './components/Primitives';
|
||||
import { MobileSelect } from './components/FormControls';
|
||||
import { useAuth } from '../auth/context';
|
||||
import { fetchTenantProfile } from '../api';
|
||||
import { adminPath, ADMIN_DATA_EXPORTS_PATH } from '../constants';
|
||||
import { adminPath, ADMIN_DATA_EXPORTS_PATH, ADMIN_PROFILE_ACCOUNT_PATH } from '../constants';
|
||||
import i18n from '../i18n';
|
||||
import { useAppearance } from '@/hooks/use-appearance';
|
||||
import { useBackNavigation } from './hooks/useBackNavigation';
|
||||
@@ -85,7 +85,7 @@ export default function MobileProfilePage() {
|
||||
<YStack space="$4">
|
||||
<YGroup {...({ borderRadius: "$4", borderWidth: 1, borderColor: borderColor, overflow: "hidden" } as any)}>
|
||||
<YGroup.Item>
|
||||
<Pressable onPress={() => navigate(adminPath('/mobile/profile/security'))}>
|
||||
<Pressable onPress={() => navigate(ADMIN_PROFILE_ACCOUNT_PATH)}>
|
||||
<ListItem
|
||||
hoverTheme
|
||||
pressTheme
|
||||
@@ -93,7 +93,7 @@ export default function MobileProfilePage() {
|
||||
paddingHorizontal="$3"
|
||||
title={
|
||||
<Text fontSize="$sm" color={textColor}>
|
||||
{t('mobileProfile.account', 'Account & security')}
|
||||
{t('mobileProfile.account', 'Account bearbeiten')}
|
||||
</Text>
|
||||
}
|
||||
iconAfter={<Settings size={18} color={subtle} />}
|
||||
@@ -216,4 +216,4 @@ export default function MobileProfilePage() {
|
||||
/>
|
||||
</MobileShell>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
NotificationPreferences,
|
||||
} from '../api';
|
||||
import { getApiErrorMessage } from '../lib/apiError';
|
||||
import { adminPath, ADMIN_HOME_PATH } from '../constants';
|
||||
import { adminPath, ADMIN_HOME_PATH, ADMIN_PROFILE_ACCOUNT_PATH } from '../constants';
|
||||
import { useAdminPushSubscription } from './hooks/useAdminPushSubscription';
|
||||
import { useDevicePermissions } from './hooks/useDevicePermissions';
|
||||
import { type PermissionStatus, type StorageStatus } from './lib/devicePermissions';
|
||||
@@ -224,7 +224,7 @@ export default function MobileSettingsPage() {
|
||||
<PillBadge tone="muted">{t('mobileSettings.tenantBadge', 'Tenant #{{id}}', { id: user.tenant_id })}</PillBadge>
|
||||
) : null}
|
||||
<XStack space="$2">
|
||||
<CTAButton label={t('settings.profile.actions.openProfile', 'Profil bearbeiten')} onPress={() => navigate(adminPath('/mobile/profile'))} />
|
||||
<CTAButton label={t('settings.profile.actions.openProfile', 'Profil bearbeiten')} onPress={() => navigate(ADMIN_PROFILE_ACCOUNT_PATH)} />
|
||||
<CTAButton label={t('settings.session.logout', 'Abmelden')} tone="ghost" onPress={() => logout({ redirect: adminPath('/logout') })} />
|
||||
</XStack>
|
||||
</MobileCard>
|
||||
|
||||
141
resources/js/admin/mobile/__tests__/ProfileAccountPage.test.tsx
Normal file
141
resources/js/admin/mobile/__tests__/ProfileAccountPage.test.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
|
||||
const backMock = vi.fn();
|
||||
|
||||
vi.mock('../hooks/useBackNavigation', () => ({
|
||||
useBackNavigation: () => backMock,
|
||||
}));
|
||||
|
||||
vi.mock('../../api', () => ({
|
||||
fetchTenantProfile: vi.fn(),
|
||||
updateTenantProfile: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('react-hot-toast', () => ({
|
||||
default: {
|
||||
error: vi.fn(),
|
||||
success: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../components/MobileShell', () => ({
|
||||
MobileShell: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../components/Primitives', () => ({
|
||||
MobileCard: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
PillBadge: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
CTAButton: ({
|
||||
label,
|
||||
onPress,
|
||||
disabled,
|
||||
}: {
|
||||
label: string;
|
||||
onPress?: () => void;
|
||||
disabled?: boolean;
|
||||
}) => (
|
||||
<button type="button" onClick={disabled ? undefined : onPress} disabled={disabled}>
|
||||
{label}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../components/FormControls', () => ({
|
||||
MobileField: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
MobileInput: ({ hasError, compact, ...props }: React.InputHTMLAttributes<HTMLInputElement> & { hasError?: boolean; compact?: boolean }) => (
|
||||
<input {...props} />
|
||||
),
|
||||
MobileSelect: ({ children, ...props }: { children: React.ReactNode }) => <select {...props}>{children}</select>,
|
||||
}));
|
||||
|
||||
vi.mock('@tamagui/stacks', () => ({
|
||||
YStack: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
XStack: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
vi.mock('@tamagui/text', () => ({
|
||||
SizableText: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
|
||||
}));
|
||||
|
||||
vi.mock('../theme', () => ({
|
||||
useAdminTheme: () => ({
|
||||
text: '#111827',
|
||||
muted: '#6b7280',
|
||||
subtle: '#94a3b8',
|
||||
danger: '#b91c1c',
|
||||
border: '#e5e7eb',
|
||||
surface: '#ffffff',
|
||||
primary: '#ff5a5f',
|
||||
accentSoft: '#fde7ea',
|
||||
}),
|
||||
}));
|
||||
|
||||
import { fetchTenantProfile, updateTenantProfile } from '../../api';
|
||||
import MobileProfileAccountPage from '../ProfileAccountPage';
|
||||
|
||||
const profileFixture = {
|
||||
id: 1,
|
||||
name: 'Test Admin',
|
||||
email: 'admin@example.com',
|
||||
preferred_locale: null,
|
||||
email_verified: true,
|
||||
email_verified_at: '2024-01-02T00:00:00.000Z',
|
||||
};
|
||||
|
||||
describe('MobileProfileAccountPage', () => {
|
||||
it('submits account updates with name, email, and locale', async () => {
|
||||
vi.mocked(fetchTenantProfile).mockResolvedValue(profileFixture);
|
||||
vi.mocked(updateTenantProfile).mockResolvedValue(profileFixture);
|
||||
|
||||
await act(async () => {
|
||||
render(<MobileProfileAccountPage />);
|
||||
});
|
||||
await screen.findByDisplayValue('Test Admin');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('profile.actions.save'));
|
||||
});
|
||||
|
||||
expect(updateTenantProfile).toHaveBeenCalledWith({
|
||||
name: 'Test Admin',
|
||||
email: 'admin@example.com',
|
||||
preferred_locale: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('submits password updates when all password fields are provided', async () => {
|
||||
vi.mocked(fetchTenantProfile).mockResolvedValue(profileFixture);
|
||||
vi.mocked(updateTenantProfile).mockResolvedValue(profileFixture);
|
||||
|
||||
await act(async () => {
|
||||
render(<MobileProfileAccountPage />);
|
||||
});
|
||||
await screen.findByDisplayValue('Test Admin');
|
||||
|
||||
const passwordInputs = screen.getAllByPlaceholderText('••••••••');
|
||||
await act(async () => {
|
||||
fireEvent.change(passwordInputs[0], { target: { value: 'old-pass' } });
|
||||
fireEvent.change(passwordInputs[1], { target: { value: 'new-pass-123' } });
|
||||
fireEvent.change(passwordInputs[2], { target: { value: 'new-pass-123' } });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('profile.actions.updatePassword')).not.toBeDisabled();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('profile.actions.updatePassword'));
|
||||
});
|
||||
|
||||
expect(updateTenantProfile).toHaveBeenCalledWith({
|
||||
name: 'Test Admin',
|
||||
email: 'admin@example.com',
|
||||
preferred_locale: null,
|
||||
current_password: 'old-pass',
|
||||
password: 'new-pass-123',
|
||||
password_confirmation: 'new-pass-123',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -35,6 +35,7 @@ const MobileEventRecapPage = React.lazy(() => import('./mobile/EventRecapPage'))
|
||||
const MobileEventAnalyticsPage = React.lazy(() => import('./mobile/EventAnalyticsPage'));
|
||||
const MobileNotificationsPage = React.lazy(() => import('./mobile/NotificationsPage'));
|
||||
const MobileProfilePage = React.lazy(() => import('./mobile/ProfilePage'));
|
||||
const MobileProfileAccountPage = React.lazy(() => import('./mobile/ProfileAccountPage'));
|
||||
const MobileBillingPage = React.lazy(() => import('./mobile/BillingPage'));
|
||||
const MobilePackageShopPage = React.lazy(() => import('./mobile/PackageShopPage'));
|
||||
const MobileSettingsPage = React.lazy(() => import('./mobile/SettingsPage'));
|
||||
@@ -212,6 +213,7 @@ export const router = createBrowserRouter([
|
||||
{ path: 'mobile/notifications', element: <MobileNotificationsPage /> },
|
||||
{ path: 'mobile/notifications/:notificationId', element: <MobileNotificationsPage /> },
|
||||
{ path: 'mobile/profile', element: <RequireAdminAccess><MobileProfilePage /></RequireAdminAccess> },
|
||||
{ path: 'mobile/profile/account', element: <RequireAdminAccess><MobileProfileAccountPage /></RequireAdminAccess> },
|
||||
{ path: 'mobile/billing', element: <RequireAdminAccess><MobileBillingPage /></RequireAdminAccess> },
|
||||
{ path: 'mobile/billing/shop', element: <RequireAdminAccess><MobilePackageShopPage /></RequireAdminAccess> },
|
||||
{ path: 'mobile/settings', element: <RequireAdminAccess><MobileSettingsPage /></RequireAdminAccess> },
|
||||
|
||||
Reference in New Issue
Block a user