diff --git a/src/lib/phone.ts b/src/lib/phone.ts
new file mode 100644
index 0000000..c4fb20f
--- /dev/null
+++ b/src/lib/phone.ts
@@ -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
+}
diff --git a/src/pages/Devices.tsx b/src/pages/Devices.tsx
index f408c89..bf49b0b 100644
--- a/src/pages/Devices.tsx
+++ b/src/pages/Devices.tsx
@@ -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({
setName(e.target.value)} />
-
- setPhone(e.target.value)} />
+
+ setPhone(e.target.value)}
+ />
@@ -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({
setName(e.target.value)} />
-
- setPhone(e.target.value)} />
+
+ setPhone(e.target.value)}
+ />
@@ -349,7 +361,7 @@ function EditDeviceDialog({
-