add number validation
This commit is contained in:
@@ -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
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user