store page fixes

This commit is contained in:
2026-09-24 15:05:48 +05:30
parent 1ec5426d15
commit 41f992b824
5 changed files with 93 additions and 79 deletions
@@ -29,7 +29,8 @@ const LocationPicker = dynamic(
const schema = z.object({ const schema = z.object({
storeName: z.string().min(1, 'Store name is required'), storeName: z.string().min(1, 'Store name is required'),
ownerName: z.string().min(1, 'Owner name is required'), ownerName: z.string().optional(),
contactNumber: z.string().optional(),
storeAddress: z.string().min(1, 'Store address is required'), storeAddress: z.string().min(1, 'Store address is required'),
routeId: z.string().min(1, 'Route is required'), routeId: z.string().min(1, 'Route is required'),
latitude: z.number(), latitude: z.number(),
@@ -55,7 +56,8 @@ function StoreEditForm({ store, routes, onSaved, onCancel }: StoreEditFormProps)
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: { defaultValues: {
storeName: store.storeName, storeName: store.storeName,
ownerName: store.ownerName, ownerName: store.ownerName ?? '',
contactNumber: store.contactNumber ?? '',
storeAddress: store.storeAddress, storeAddress: store.storeAddress,
routeId: store.routeId, routeId: store.routeId,
latitude: store.latitude, latitude: store.latitude,
@@ -74,6 +76,8 @@ function StoreEditForm({ store, routes, onSaved, onCancel }: StoreEditFormProps)
await storesApi.update(store.id, { await storesApi.update(store.id, {
...values, ...values,
segment: values.segment || undefined, segment: values.segment || undefined,
ownerName: values.ownerName || null,
contactNumber: values.contactNumber || null,
}) })
toast.add({ title: 'Store updated', type: 'success' }) toast.add({ title: 'Store updated', type: 'success' })
onSaved() onSaved()
@@ -108,25 +112,6 @@ function StoreEditForm({ store, routes, onSaved, onCancel }: StoreEditFormProps)
)} )}
/> />
<FormField
control={form.control}
name="ownerName"
label="Owner Name"
required
render={({ field, invalid }) => (
<Input
id="ownerName"
name={field.name}
ref={field.ref}
value={field.value as string}
onChange={field.onChange}
onBlur={field.onBlur}
placeholder="e.g. Kamal Perera"
aria-invalid={invalid}
/>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="storeAddress" name="storeAddress"
@@ -167,6 +152,43 @@ function StoreEditForm({ store, routes, onSaved, onCancel }: StoreEditFormProps)
)} )}
/> />
<FormField
control={form.control}
name="ownerName"
label="Owner Name"
render={({ field, invalid }) => (
<Input
id="ownerName"
name={field.name}
ref={field.ref}
value={field.value as string}
onChange={field.onChange}
onBlur={field.onBlur}
placeholder="e.g. Kamal Perera"
aria-invalid={invalid}
/>
)}
/>
<FormField
control={form.control}
name="contactNumber"
label="Contact Number"
render={({ field, invalid }) => (
<Input
id="contactNumber"
name={field.name}
ref={field.ref}
type="tel"
value={(field.value as string) ?? ""}
onChange={field.onChange}
onBlur={field.onBlur}
placeholder="e.g. 0771234567"
aria-invalid={invalid}
/>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="creditLimit" name="creditLimit"
@@ -9,7 +9,7 @@ import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { toast } from '@/components/ui/toast' import { toast } from '@/components/ui/toast'
import { stores as storesApi, routes as routesApi, getApiError, getPaginatedItems } from '@/lib/api/index' import { stores as storesApi, routes as routesApi, getApiError, getPaginatedItems } from '@/lib/api/index'
import type { StoreResponse, KycStatus, RouteResponse } from '@/types/api' import type { StoreResponse, RouteResponse } from '@/types/api'
const LocationPicker = dynamic( const LocationPicker = dynamic(
() => import('../_components/location-picker').then((m) => m.LocationPicker), () => import('../_components/location-picker').then((m) => m.LocationPicker),
@@ -22,12 +22,6 @@ const fmtDate = (v: string) =>
const fmtMoney = (v: number) => const fmtMoney = (v: number) =>
`Rs. ${v.toLocaleString('en-LK', { minimumFractionDigits: 2 })}` `Rs. ${v.toLocaleString('en-LK', { minimumFractionDigits: 2 })}`
function kycVariant(status: KycStatus): 'default' | 'secondary' | 'destructive' {
if (status === 'Verified') return 'default'
if (status === 'Rejected') return 'destructive'
return 'secondary'
}
function DetailRow({ label, children }: { label: string; children: React.ReactNode }) { function DetailRow({ label, children }: { label: string; children: React.ReactNode }) {
return ( return (
<div className="flex flex-col gap-0.5 py-3 border-b last:border-0"> <div className="flex flex-col gap-0.5 py-3 border-b last:border-0">
@@ -95,7 +89,6 @@ export default function StoreViewPage() {
<Badge variant={store.isActive ? 'default' : 'secondary'}> <Badge variant={store.isActive ? 'default' : 'secondary'}>
{store.isActive ? 'Active' : 'Inactive'} {store.isActive ? 'Active' : 'Inactive'}
</Badge> </Badge>
<Badge variant={kycVariant(store.kycStatus)}>{store.kycStatus}</Badge>
</> </>
)} )}
</div> </div>
@@ -136,7 +129,8 @@ export default function StoreViewPage() {
{/* Left — store details */} {/* Left — store details */}
<div className="lg:col-span-1"> <div className="lg:col-span-1">
<div className="rounded-2xl border px-4"> <div className="rounded-2xl border px-4">
<DetailRow label="Owner">{store.ownerName}</DetailRow> <DetailRow label="Owner">{store.ownerName ?? "—"}</DetailRow>
<DetailRow label="Contact">{store.contactNumber ?? "—"}</DetailRow>
<DetailRow label="Address">{store.storeAddress}</DetailRow> <DetailRow label="Address">{store.storeAddress}</DetailRow>
<DetailRow label="Route"> <DetailRow label="Route">
{routeName ?? <span className="font-mono text-xs">{store.routeId}</span>} {routeName ?? <span className="font-mono text-xs">{store.routeId}</span>}
@@ -145,9 +139,6 @@ export default function StoreViewPage() {
<DetailRow label="Segment">{store.segment}</DetailRow> <DetailRow label="Segment">{store.segment}</DetailRow>
)} )}
<DetailRow label="Credit Limit">{fmtMoney(store.creditLimit)}</DetailRow> <DetailRow label="Credit Limit">{fmtMoney(store.creditLimit)}</DetailRow>
<DetailRow label="KYC Status">
<Badge variant={kycVariant(store.kycStatus)}>{store.kycStatus}</Badge>
</DetailRow>
<DetailRow label="Registered">{fmtDate(store.createdAt)}</DetailRow> <DetailRow label="Registered">{fmtDate(store.createdAt)}</DetailRow>
</div> </div>
</div> </div>
@@ -35,7 +35,7 @@ const LocationPicker = dynamic(
const schema = z.object({ const schema = z.object({
storeName: z.string().min(1, 'Store name is required'), storeName: z.string().min(1, 'Store name is required'),
ownerName: z.string().min(1, 'Owner name is required'), ownerName: z.string().optional(),
storeAddress: z.string().min(1, 'Store address is required'), storeAddress: z.string().min(1, 'Store address is required'),
routeId: z.string().min(1, 'Route is required'), routeId: z.string().min(1, 'Route is required'),
latitude: z.number(), latitude: z.number(),
@@ -83,7 +83,7 @@ export function StoreSheet({ open, onClose, store, onSuccess }: StoreSheetProps)
if (store) form.reset({ if (store) form.reset({
storeName: store.storeName, storeName: store.storeName,
ownerName: store.ownerName, ownerName: store.ownerName ?? '',
storeAddress: store.storeAddress, storeAddress: store.storeAddress,
routeId: store.routeId, routeId: store.routeId,
latitude: store.latitude, latitude: store.latitude,
@@ -11,17 +11,11 @@ import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/toast' import { toast } from '@/components/ui/toast'
import { stores as storesApi, routes as routesApi, getApiError, getPaginatedItems } from '@/lib/api/index' import { stores as storesApi, routes as routesApi, getApiError, getPaginatedItems } from '@/lib/api/index'
import type { StoreResponse, KycStatus, RouteResponse } from '@/types/api' import type { StoreResponse, RouteResponse } from '@/types/api'
import { StoreSheet } from './store-sheet' import { StoreSheet } from './store-sheet'
import { StoreVisitHistorySheet } from './store-visit-history-sheet' import { StoreVisitHistorySheet } from './store-visit-history-sheet'
function kycBadgeVariant(status: KycStatus): 'default' | 'secondary' | 'destructive' {
if (status === 'Verified') return 'default'
if (status === 'Rejected') return 'destructive'
return 'secondary'
}
export function StoresClient() { export function StoresClient() {
const router = useRouter() const router = useRouter()
const [data, setData] = useState<StoreResponse[]>([]) const [data, setData] = useState<StoreResponse[]>([])
@@ -120,17 +114,6 @@ export function StoresClient() {
<span className="text-sm">{routeNameById.get(row.routeId) ?? row.routeId}</span> <span className="text-sm">{routeNameById.get(row.routeId) ?? row.routeId}</span>
), ),
}, },
{
key: 'kycStatus',
title: 'KYC',
fields: ['kycStatus'],
width: 130,
cellRender: (row) => (
<Badge variant={kycBadgeVariant(row.kycStatus)}>
{row.kycStatus}
</Badge>
),
},
{ {
key: 'status', key: 'status',
title: 'Status', title: 'Status',
@@ -155,11 +138,6 @@ export function StoresClient() {
label: 'Edit store', label: 'Edit store',
onClick: (store) => router.push(`/stores/${store.id}/edit`), onClick: (store) => router.push(`/stores/${store.id}/edit`),
}, },
{
action: 'confirm',
label: 'View visit history',
onClick: openHistory,
},
{ {
action: 'deactivate', action: 'deactivate',
label: 'Deactivate store', label: 'Deactivate store',
@@ -182,7 +160,7 @@ export function StoresClient() {
<div> <div>
<h1 className="text-2xl font-semibold text-foreground">Stores</h1> <h1 className="text-2xl font-semibold text-foreground">Stores</h1>
<p className="text-sm text-muted-foreground mt-0.5"> <p className="text-sm text-muted-foreground mt-0.5">
Retail store accounts, credit limits, and KYC status Retail store accounts, credit limits and balances
</p> </p>
</div> </div>
<Button onClick={() => router.push('/stores/new')}> <Button onClick={() => router.push('/stores/new')}>
@@ -195,6 +173,7 @@ export function StoresClient() {
config={tableConfig} config={tableConfig}
data={data} data={data}
loading={loading} loading={loading}
onRowClick={openHistory}
totalCount={total} totalCount={total}
onServerSideChange={({ page: p }) => { onServerSideChange={({ page: p }) => {
setPage(p.page) setPage(p.page)
@@ -29,7 +29,8 @@ const LocationPicker = dynamic(
const schema = z.object({ const schema = z.object({
storeName: z.string().min(1, 'Store name is required'), storeName: z.string().min(1, 'Store name is required'),
ownerName: z.string().min(1, 'Owner name is required'), ownerName: z.string().optional(),
contactNumber: z.string().optional(),
storeAddress: z.string().min(1, 'Store address is required'), storeAddress: z.string().min(1, 'Store address is required'),
routeId: z.string().min(1, 'Route is required'), routeId: z.string().min(1, 'Route is required'),
latitude: z.number(), latitude: z.number(),
@@ -50,6 +51,7 @@ export default function NewStorePage() {
defaultValues: { defaultValues: {
storeName: '', storeName: '',
ownerName: '', ownerName: '',
contactNumber: '',
storeAddress: '', storeAddress: '',
routeId: '', routeId: '',
latitude: 0, latitude: 0,
@@ -76,6 +78,8 @@ export default function NewStorePage() {
await storesApi.create({ await storesApi.create({
...values, ...values,
segment: values.segment || undefined, segment: values.segment || undefined,
ownerName: values.ownerName || null,
contactNumber: values.contactNumber || null,
}) })
toast.add({ title: 'Store created', type: 'success' }) toast.add({ title: 'Store created', type: 'success' })
router.push('/stores') router.push('/stores')
@@ -129,25 +133,6 @@ export default function NewStorePage() {
)} )}
/> />
<FormField
control={form.control}
name="ownerName"
label="Owner Name"
required
render={({ field, invalid }) => (
<Input
id="ownerName"
name={field.name}
ref={field.ref}
value={field.value as string}
onChange={field.onChange}
onBlur={field.onBlur}
placeholder="e.g. Kamal Perera"
aria-invalid={invalid}
/>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="storeAddress" name="storeAddress"
@@ -188,6 +173,43 @@ export default function NewStorePage() {
)} )}
/> />
<FormField
control={form.control}
name="ownerName"
label="Owner Name"
render={({ field, invalid }) => (
<Input
id="ownerName"
name={field.name}
ref={field.ref}
value={field.value as string}
onChange={field.onChange}
onBlur={field.onBlur}
placeholder="e.g. Kamal Perera"
aria-invalid={invalid}
/>
)}
/>
<FormField
control={form.control}
name="contactNumber"
label="Contact Number"
render={({ field, invalid }) => (
<Input
id="contactNumber"
name={field.name}
ref={field.ref}
type="tel"
value={(field.value as string) ?? ""}
onChange={field.onChange}
onBlur={field.onBlur}
placeholder="e.g. 0771234567"
aria-invalid={invalid}
/>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="creditLimit" name="creditLimit"