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 { toast } from 'sonner'
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 { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input'
@@ -187,7 +188,8 @@ function AddDeviceDialog({
}
}, [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() {
if (!valid) return
@@ -197,7 +199,7 @@ function AddDeviceDialog({
mac_address: mac.trim(),
group,
name: name.trim(),
phone: phone.trim(),
phone: normalizePhone(phone) ?? phone.trim(),
alias: alias.trim() || null,
})
toast.success(`Device ${mac.trim()} added`)
@@ -233,8 +235,12 @@ function AddDeviceDialog({
<Field label="Name" required>
<Input value={name} onChange={(e) => setName(e.target.value)} />
</Field>
<Field label="Phone" required>
<Input value={phone} onChange={(e) => setPhone(e.target.value)} />
<Field label="Phone" required error={phoneErr}>
<Input
placeholder="9XXXXXX"
value={phone}
onChange={(e) => setPhone(e.target.value)}
/>
</Field>
</div>
<Field label="Device alias (optional)">
@@ -284,8 +290,10 @@ function EditDeviceDialog({
}
}, [device])
const phoneErr = phone.trim() ? phoneError(phone) : 'Phone is required'
async function submit() {
if (!device) return
if (!device || phoneErr) return
setBusy(true)
try {
await api.devices.edit({
@@ -293,7 +301,7 @@ function EditDeviceDialog({
group: group || undefined,
status,
name,
phone,
phone: normalizePhone(phone) ?? phone,
alias,
})
toast.success(`Device ${device.mac_address} updated`)
@@ -337,8 +345,12 @@ function EditDeviceDialog({
<Field label="Name">
<Input value={name} onChange={(e) => setName(e.target.value)} />
</Field>
<Field label="Phone">
<Input value={phone} onChange={(e) => setPhone(e.target.value)} />
<Field label="Phone" error={phoneErr}>
<Input
placeholder="9XXXXXX"
value={phone}
onChange={(e) => setPhone(e.target.value)}
/>
</Field>
</div>
<Field label="Device alias">
@@ -349,7 +361,7 @@ function EditDeviceDialog({
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<Button onClick={submit} disabled={busy}>
<Button onClick={submit} disabled={busy || !!phoneErr}>
{busy && <Loader2 className="h-4 w-4 animate-spin" />}
Save changes
</Button>
@@ -414,10 +426,12 @@ function DeleteDeviceDialog({
function Field({
label,
required,
error,
children,
}: {
label: string
required?: boolean
error?: string | null
children: ReactNode
}) {
return (
@@ -427,6 +441,7 @@ function Field({
{required && <span className="text-destructive"> *</span>}
</Label>
{children}
{error && <p className="text-xs text-destructive">{error}</p>}
</div>
)
}