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 { 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user