Files
loyaly_cutomerweb/src/features/settings/components/ProfileForm.tsx
2026-09-25 16:31:10 +05:30

166 lines
5.3 KiB
TypeScript

'use client';
import {useState} from 'react';
import {VStack, HStack} from '@astryxdesign/core/Layout';
import {FormLayout} from '@astryxdesign/core/FormLayout';
import {TextInput} from '@astryxdesign/core/TextInput';
import {NumberInput} from '@astryxdesign/core/NumberInput';
import {Button} from '@astryxdesign/core/Button';
import {Text} from '@astryxdesign/core/Text';
import {Divider} from '@astryxdesign/core/Divider';
import {useToast} from '@astryxdesign/core/Toast';
import {StaticPanel} from '@/shared/components/patterns/PanelCard';
import {settingsService} from '@/features/settings/services/settingsService';
import type {ProfileFieldErrors} from '@/features/settings/services/settingsService';
import type {MerchantProfile} from '@/features/settings/types/settings';
/**
* Merchant profile.
*
* `initialData` arrives from a Server Component that read the record
* DIRECTLY — no HTTP round trip — so the form paints filled on first render
* rather than flashing empty inputs. This is the hybrid path the data layer
* was designed for: dashboards fetch on the client because their scope
* changes constantly, but a settings form has exactly one server-known state
* at page load and should use it.
*
* Status is only ever set on submit, never at initial paint — Astryx's
* FieldStatus breaks SSR hydration when it is server-rendered with a status
* already applied (see AGENTS.md).
*/
export function ProfileForm({initialData}: {initialData: MerchantProfile}) {
const toast = useToast();
const [form, setForm] = useState(initialData);
const [isSaving, setIsSaving] = useState(false);
const [errors, setErrors] = useState<ProfileFieldErrors>({});
const set = <K extends keyof MerchantProfile>(
key: K,
value: MerchantProfile[K],
) => setForm((f) => ({...f, [key]: value}));
const isDirty = JSON.stringify(form) !== JSON.stringify(initialData);
async function save() {
// Validation and transport both live in settingsService — this component
// decides what to render, never what a valid profile is or where it goes.
// The raw fetch that used to sit here was the last one in the codebase.
setIsSaving(true);
const result = await settingsService.saveProfile(form);
setIsSaving(false);
if (result.ok) {
setErrors({});
toast({body: 'Profile saved'});
return;
}
setErrors(result.errors);
if (result.message) {
toast({type: 'error', body: result.message});
}
}
return (
<StaticPanel
title="Business profile"
subtitle="Shown on receipts and used for LYT settlement."
actions={
<HStack gap={2} vAlign="center">
{isDirty ? (
<Text size="sm" color="secondary">
Unsaved changes
</Text>
) : null}
<Button
variant="secondary"
size="sm"
label="Reset"
isDisabled={!isDirty || isSaving}
onClick={() => {
setForm(initialData);
setErrors({});
}}
/>
<Button
variant="primary"
size="sm"
label="Save changes"
isDisabled={!isDirty}
isLoading={isSaving}
onClick={save}
/>
</HStack>
}
>
<VStack gap={5}>
<FormLayout direction="horizontal">
<TextInput
label="Business name"
value={form.businessName}
onChange={(v) => set('businessName', v)}
isRequired
status={
errors.businessName
? {type: 'error', message: errors.businessName}
: undefined
}
/>
<TextInput
label="Contact name"
value={form.contactName}
onChange={(v) => set('contactName', v)}
/>
</FormLayout>
<FormLayout direction="horizontal">
<TextInput
label="Email"
type="email"
value={form.email}
onChange={(v) => set('email', v)}
isRequired
status={
errors.email ? {type: 'error', message: errors.email} : undefined
}
/>
<TextInput
label="Phone"
value={form.phone}
onChange={(v) => set('phone', v)}
/>
</FormLayout>
<Divider />
<FormLayout direction="horizontal">
<TextInput
label="GSTIN"
value={form.gstin}
onChange={(v) => set('gstin', v.toUpperCase())}
description="15-character GST registration number"
status={
errors.gstin ? {type: 'error', message: errors.gstin} : undefined
}
/>
<TextInput
label="Timezone"
value={form.timezone}
onChange={(v) => set('timezone', v)}
description="All reports are rendered in this zone"
/>
</FormLayout>
<NumberInput
label="LYTs per ₹100 spent"
value={form.lytsPerHundred}
onChange={(v) => set('lytsPerHundred', v ?? 0)}
min={0}
max={100}
description="1 LYT = ₹1. At 5, a ₹1,000 basket earns 50 LYTs — ₹50 of future liability."
width={320}
/>
</VStack>
</StaticPanel>
);
}