add number validation

This commit is contained in:
2026-07-31 23:27:26 +05:00
parent 46f88ca04c
commit 428536fb1b
2 changed files with 47 additions and 9 deletions
+23
View File
@@ -0,0 +1,23 @@
// Mirrors the API's phone rule (radapi schemas._normalize_phone):
// 7 digits starting with 9 or 7. A 960/+960 country code is stripped ONLY when the
// number is 10 digits (960 + 7) — a bare 7-digit number like 9601234 is a valid
// local number and is never stripped.
export const PHONE_ERROR = 'Enter a valid phone number'
/** Returns the normalized 7-digit local number, or null if invalid. */
export function normalizePhone(raw: string): string | null {
let digits = (raw ?? '').replace(/\D/g, '')
if (digits.length === 10 && digits.startsWith('960')) {
digits = digits.slice(3)
}
if (digits.length === 7 && (digits[0] === '9' || digits[0] === '7')) {
return digits
}
return null
}
/** Error message if invalid, else null. */
export function phoneError(raw: string): string | null {
return normalizePhone(raw) === null ? PHONE_ERROR : null
}
+24 -9
View File
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState, type ReactNode } from 'react'
import { Loader2, Pencil, Plus, RefreshCw, Trash2 } from 'lucide-react' import { Loader2, Pencil, Plus, RefreshCw, Trash2 } from 'lucide-react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { api, ApiError, type Device, type DeviceStatus, type Vlan } from '@/lib/api' import { api, ApiError, type Device, type DeviceStatus, type Vlan } from '@/lib/api'
import { normalizePhone, phoneError } from '@/lib/phone'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
@@ -187,7 +188,8 @@ function AddDeviceDialog({
} }
}, [open]) }, [open])
const valid = mac.trim() && group && name.trim() && phone.trim() const phoneErr = phone.trim() ? phoneError(phone) : null
const valid = mac.trim() && group && name.trim() && phone.trim() && !phoneErr
async function submit() { async function submit() {
if (!valid) return if (!valid) return
@@ -197,7 +199,7 @@ function AddDeviceDialog({
mac_address: mac.trim(), mac_address: mac.trim(),
group, group,
name: name.trim(), name: name.trim(),
phone: phone.trim(), phone: normalizePhone(phone) ?? phone.trim(),
alias: alias.trim() || null, alias: alias.trim() || null,
}) })
toast.success(`Device ${mac.trim()} added`) toast.success(`Device ${mac.trim()} added`)
@@ -233,8 +235,12 @@ function AddDeviceDialog({
<Field label="Name" required> <Field label="Name" required>
<Input value={name} onChange={(e) => setName(e.target.value)} /> <Input value={name} onChange={(e) => setName(e.target.value)} />
</Field> </Field>
<Field label="Phone" required> <Field label="Phone" required error={phoneErr}>
<Input value={phone} onChange={(e) => setPhone(e.target.value)} /> <Input
placeholder="9XXXXXX"
value={phone}
onChange={(e) => setPhone(e.target.value)}
/>
</Field> </Field>
</div> </div>
<Field label="Device alias (optional)"> <Field label="Device alias (optional)">
@@ -284,8 +290,10 @@ function EditDeviceDialog({
} }
}, [device]) }, [device])
const phoneErr = phone.trim() ? phoneError(phone) : 'Phone is required'
async function submit() { async function submit() {
if (!device) return if (!device || phoneErr) return
setBusy(true) setBusy(true)
try { try {
await api.devices.edit({ await api.devices.edit({
@@ -293,7 +301,7 @@ function EditDeviceDialog({
group: group || undefined, group: group || undefined,
status, status,
name, name,
phone, phone: normalizePhone(phone) ?? phone,
alias, alias,
}) })
toast.success(`Device ${device.mac_address} updated`) toast.success(`Device ${device.mac_address} updated`)
@@ -337,8 +345,12 @@ function EditDeviceDialog({
<Field label="Name"> <Field label="Name">
<Input value={name} onChange={(e) => setName(e.target.value)} /> <Input value={name} onChange={(e) => setName(e.target.value)} />
</Field> </Field>
<Field label="Phone"> <Field label="Phone" error={phoneErr}>
<Input value={phone} onChange={(e) => setPhone(e.target.value)} /> <Input
placeholder="9XXXXXX"
value={phone}
onChange={(e) => setPhone(e.target.value)}
/>
</Field> </Field>
</div> </div>
<Field label="Device alias"> <Field label="Device alias">
@@ -349,7 +361,7 @@ function EditDeviceDialog({
<Button variant="outline" onClick={onClose}> <Button variant="outline" onClick={onClose}>
Cancel Cancel
</Button> </Button>
<Button onClick={submit} disabled={busy}> <Button onClick={submit} disabled={busy || !!phoneErr}>
{busy && <Loader2 className="h-4 w-4 animate-spin" />} {busy && <Loader2 className="h-4 w-4 animate-spin" />}
Save changes Save changes
</Button> </Button>
@@ -414,10 +426,12 @@ function DeleteDeviceDialog({
function Field({ function Field({
label, label,
required, required,
error,
children, children,
}: { }: {
label: string label: string
required?: boolean required?: boolean
error?: string | null
children: ReactNode children: ReactNode
}) { }) {
return ( return (
@@ -427,6 +441,7 @@ function Field({
{required && <span className="text-destructive"> *</span>} {required && <span className="text-destructive"> *</span>}
</Label> </Label>
{children} {children}
{error && <p className="text-xs text-destructive">{error}</p>}
</div> </div>
) )
} }