feat(ayarlar): hane ve üyeler, davet bağlantısı, hesap ve şifre, bütçe varsayılanları

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-13 22:32:28 +03:00
parent 9daeff3eb5
commit 77d7a081b3
31 changed files with 1564 additions and 21 deletions
@@ -0,0 +1,151 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { Loader2 } from 'lucide-react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { FormError, FormField, PasswordInput } from '@/components/form-field'
import { LogoutButton } from '@/components/layout/app-layout'
import { PageLoading } from '@/components/page'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Section } from '@/features/planning/plan-read'
import { useMe } from '@/lib/auth'
import { useChangePassword, useUpdateProfile } from '@/lib/household'
import { t } from '@/lib/strings'
import type { MeResponse } from '@/types/MeResponse'
import { nameSchema, passwordSchema, type NameForm, type PasswordForm } from './schemas'
import { SettingsSubpage } from './subpage'
const s = t.settings.account
const message = (e: unknown) => (e instanceof Error ? e.message : String(e))
/** Ayarlar → Hesabım: ad, e-posta (salt okunur), şifre değiştirme, çıkış */
export function AccountPage() {
const me = useMe()
return (
<SettingsSubpage title={s.title} description={s.description}>
{!me.data ? (
<PageLoading />
) : (
<div className="max-w-xl space-y-4">
<Profile me={me.data} />
<ChangePassword />
<div className="flex justify-end">
<LogoutButton />
</div>
</div>
)}
</SettingsSubpage>
)
}
function Profile({ me }: { me: MeResponse }) {
const update = useUpdateProfile()
const {
register,
handleSubmit,
setError,
reset,
formState: { errors, isDirty },
} = useForm<NameForm>({
resolver: zodResolver(nameSchema),
mode: 'onTouched',
defaultValues: { name: me.user.name },
})
const onSubmit = handleSubmit(async (v) => {
try {
const next = await update.mutateAsync(v.name)
reset({ name: next.user.name })
toast.success(s.saved)
} catch (e) {
setError('name', { message: message(e) })
}
})
return (
<Section title={s.profile}>
<form onSubmit={onSubmit} className="space-y-4" noValidate>
<FormField label={s.name} error={errors.name}>
{(f) => <Input {...f} {...register('name')} autoComplete="name" maxLength={80} />}
</FormField>
<FormField label={s.email} hint={s.emailHint}>
{(f) => <Input {...f} value={me.user.email} readOnly disabled />}
</FormField>
<p className="text-sm">
<span className="text-muted-foreground">{s.role}: </span>
{me.user.role === 'owner' ? t.account.owner : t.account.member} · {me.household.name}
</p>
<Button type="submit" disabled={!isDirty || update.isPending}>
{update.isPending && <Loader2 className="animate-spin" aria-hidden />}
{s.save}
</Button>
</form>
</Section>
)
}
function ChangePassword() {
const change = useChangePassword()
const {
register,
handleSubmit,
setError,
reset,
formState: { errors, isSubmitting },
} = useForm<PasswordForm>({
resolver: zodResolver(passwordSchema),
mode: 'onTouched',
defaultValues: { current_password: '', new_password: '', repeat: '' },
})
const onSubmit = handleSubmit(async (v) => {
try {
await change.mutateAsync({
current_password: v.current_password,
new_password: v.new_password,
})
reset()
toast.success(s.changed)
} catch (e) {
const msg = message(e)
// "Mevcut şifre hatalı" → ilgili alana; diğerleri formun altına
if (/mevcut/i.test(msg)) setError('current_password', { message: msg }, { shouldFocus: true })
else setError('root', { message: msg })
}
})
return (
<Section title={s.password}>
<form onSubmit={onSubmit} className="space-y-4" noValidate>
<p className="text-sm text-muted-foreground">{s.otherDevices}</p>
{/* Parola yöneticileri için gizli kullanıcı adı alanı */}
<input
type="text"
autoComplete="username"
className="hidden"
readOnly
aria-hidden
tabIndex={-1}
/>
<FormField label={s.current} error={errors.current_password}>
{(f) => (
<PasswordInput
{...f}
{...register('current_password')}
autoComplete="current-password"
/>
)}
</FormField>
<FormField label={s.next} error={errors.new_password} hint={s.nextHint}>
{(f) => (
<PasswordInput {...f} {...register('new_password')} autoComplete="new-password" />
)}
</FormField>
<FormField label={s.repeat} error={errors.repeat}>
{(f) => <PasswordInput {...f} {...register('repeat')} autoComplete="new-password" />}
</FormField>
<FormError message={errors.root?.message} />
<Button type="submit" disabled={isSubmitting}>
{isSubmitting && <Loader2 className="animate-spin" aria-hidden />}
{s.change}
</Button>
</form>
</Section>
)
}