diff --git a/src/app/devices/page.tsx b/src/app/devices/page.tsx index a607bb9..f939e02 100644 --- a/src/app/devices/page.tsx +++ b/src/app/devices/page.tsx @@ -1,259 +1,293 @@ "use client" -import { useMemo, useState, useRef, useEffect } from 'react' -import { api, DeviceDto } from '@/lib/api' -import { Settings, CheckCircle2, ArrowRight, Calendar, BarChart3, Activity, RotateCcw } from 'lucide-react' +import { useEffect, useState } from 'react' +import { useRouter } from 'next/navigation' +import { api, DeviceDto, PagedResult } from '@/lib/api' +import { Settings, Calendar, LogOut, ArrowRight, Search, ChevronRight, ChevronLeft } from 'lucide-react' import Link from 'next/link' import Loading from '@/components/Loading' export default function DevicesPage() { - const [loading, setLoading] = useState(false) - const [deviceName, setDeviceName] = useState('') - const [password, setPassword] = useState('') - const [selected, setSelected] = useState(null) + const router = useRouter() + const [pagedResult, setPagedResult] = useState | null>(null) + const [loading, setLoading] = useState(true) const [error, setError] = useState(null) + const [user, setUser] = useState<{ id: number; mobile: string; name: string; family: string; role: number } | null>(null) + const [searchTerm, setSearchTerm] = useState('') + const [currentPage, setCurrentPage] = useState(1) + const [searchInput, setSearchInput] = useState('') - const deviceNameRef = useRef(null) - const passwordRef = useRef(null) + const pageSize = 10 - const canSubmit = useMemo(() => deviceName.trim().length > 0 && password.trim().length > 0, [deviceName, password]) - - // بررسی auto-fill پس از لود صفحه - useEffect(() => { - const checkAutoFill = () => { - setTimeout(() => { - if (deviceNameRef.current) { - const filledDeviceName = deviceNameRef.current.value - if (filledDeviceName && filledDeviceName !== deviceName) { - setDeviceName(filledDeviceName) - } - } - if (passwordRef.current) { - const filledPassword = passwordRef.current.value - if (filledPassword && filledPassword !== password) { - setPassword(filledPassword) - } - } - }, 100) - } - - checkAutoFill() - window.addEventListener('load', checkAutoFill) - - return () => { - window.removeEventListener('load', checkAutoFill) - } - }, [deviceName, password]) - - // بررسی تغییرات در فیلدها (برای مرورگرهایی که بعد از تعامل auto-fill می‌کنند) - const handleInputChange = (e: React.FormEvent) => { - const target = e.target as HTMLInputElement - if (target.name === 'deviceName') { - setDeviceName(target.value) - } else if (target.name === 'password') { - setPassword(target.value) - } - } - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault() - setError(null) - - // گرفتن آخرین مقادیر از refها در صورت لزوم - const currentDeviceName = deviceNameRef.current?.value || deviceName - const currentPassword = passwordRef.current?.value || password - - if (!currentDeviceName.trim() || !currentPassword.trim()) { - setError('لطفاً نام دستگاه و رمز عبور را وارد کنید') - return - } + const fetchDevices = async (page: number, search?: string) => { + if (!user) return setLoading(true) + setError(null) try { - // فراخوانی API برای بررسی دستگاه - const device = await api.CheckDevice(currentDeviceName.trim(), currentPassword.trim()) + const result = await api.getDevicesFiltered({ + userId: user.id, + search: search || undefined, + page, + pageSize + }) - if (device !=undefined) { - setSelected(device) + if (result.items.length === 0 && page === 1) { + setError('شما هیچ دستگاهی ندارید. لطفاً با پشتیبانی تماس بگیرید.') + } else if (result.items.length === 1 && result.totalCount === 1 && !search) { + // Single device - redirect to calendar + router.push(`/calendar?deviceId=${result.items[0].id}`) + return } else { - setError('دستگاه یافت نشد یا رمز عبور نادرست است') + setPagedResult(result) } - } catch (error) { - console.error('Error checking device:', error) - setError('خطا در ارتباط با سرور') + } catch (err) { + console.error('Error fetching devices:', err) + setError('خطا در دریافت لیست دستگاه‌ها') } finally { setLoading(false) } } - function handleReset() { - setSelected(null) - setDeviceName('') - setPassword('') - setError(null) + useEffect(() => { + // Check authentication + const token = localStorage.getItem('authToken') + const userStr = localStorage.getItem('user') - // پاک کردن refها - if (deviceNameRef.current) deviceNameRef.current.value = '' - if (passwordRef.current) passwordRef.current.value = '' + if (!token || !userStr) { + router.push('/') + return + } + + try { + const userData = JSON.parse(userStr) + setUser(userData) + } catch { + router.push('/') + } + }, [router]) + + useEffect(() => { + if (user) { + fetchDevices(currentPage, searchTerm || undefined) + } + }, [user, currentPage, searchTerm]) + + const handleSearch = (e: React.FormEvent) => { + e.preventDefault() + setSearchTerm(searchInput) + setCurrentPage(1) } - if (loading) { - return + const handleLogout = () => { + localStorage.removeItem('authToken') + localStorage.removeItem('user') + router.push('/') + } + + const totalPages = pagedResult ? Math.ceil(pagedResult.totalCount / pageSize) : 0 + const showPagination = pagedResult && pagedResult.totalCount > pageSize + + if (loading && !pagedResult) { + return + } + + if (error && (!pagedResult || pagedResult.items.length === 0)) { + return ( +
+
+
+
+ +
+

خطا

+

{error}

+ +
+
+
+ ) } return ( -
-
- {/* Main Card */} -
- {/* Title Section */} -
-
- {selected ? ( - - ) : ( - - )} -
-

- {selected ? 'دستگاه فعال' : 'انتخاب دستگاه'} +
+
+ {/* Header */} +
+
+

+ انتخاب دستگاه

-

- {selected - ? `دستگاه "${selected.deviceName}" با موفقیت تأیید شد` - : 'نام دستگاه و رمز عبور را وارد کنید' - } -

+ {user && ( +

+ {user.name} {user.family} ({user.mobile}) +

+ )}
+ +
- {/* نمایش فرم فقط وقتی دستگاه انتخاب نشده */} - {!selected ? ( -
- {/* Input Fields */} -
-
- - -
-
- - -
-
- - {/* Error Message */} - {error && ( -
- {error} -
- )} - - {/* Confirm Button */} + {/* Search */} +
+ +
+ + setSearchInput(e.target.value)} + placeholder="جستجو در نام دستگاه، نام صاحب، نام خانوادگی صاحب، موقعیت..." + className="w-full pr-12 pl-4 py-3 rounded-xl border-2 border-gray-200 bg-gray-50 focus:border-green-500 focus:bg-white focus:outline-none focus:ring-2 focus:ring-green-500/20 transition-all duration-200" + /> +
+ + + {searchTerm && ( +
+ + نتایج جستجو برای: {searchTerm} + - - ) : ( - /* نمایش دکمه‌های عملیات وقتی دستگاه انتخاب شده */ -
- {/* اطلاعات دستگاه فعال */} -
-
- -

{selected.deviceName}

-
-

دستگاه فعال و آماده استفاده

-
- - {/* Main Action Button */} - - - داده‌های امروز - - - {/* Secondary Action Buttons */} -
- - - تنظیمات دستگاه - -
- - - تقویم ماه - - - - انتخاب ماه - -
-
- - {/* دکمه تغییر دستگاه */} -
- -
)}
+ {/* Results Count */} + {pagedResult && ( +
+ نمایش {pagedResult.items.length} از {pagedResult.totalCount} دستگاه +
+ )} + + {/* Devices Grid */} + {pagedResult && pagedResult.items.length > 0 ? ( + <> +
+ {pagedResult.items.map((device) => ( + +
+
+ +
+
+

+ {device.deviceName} +

+

+ {device.location || 'بدون موقعیت'} +

+
+ {device.userName} {device.userFamily} +
+
+
+ +
+
+
+ + ))} +
+ + {/* Pagination */} + {showPagination && ( +
+ + +
+ {Array.from({ length: Math.min(5, totalPages) }, (_, i) => { + let pageNum: number + if (totalPages <= 5) { + pageNum = i + 1 + } else if (currentPage <= 3) { + pageNum = i + 1 + } else if (currentPage >= totalPages - 2) { + pageNum = totalPages - 4 + i + } else { + pageNum = currentPage - 2 + i + } + + return ( + + ) + })} +
+ + +
+ )} + + ) : ( + !loading && ( +
+

هیچ دستگاهی یافت نشد

+
+ ) + )} + {/* Back to Home */}
) -} \ No newline at end of file +} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..fba7906 --- /dev/null +++ b/src/app/login/page.tsx @@ -0,0 +1,162 @@ +"use client" +import { useState, useRef, useEffect } from 'react' +import { api } from '@/lib/api' +import { Smartphone, ArrowLeft, ArrowRight } from 'lucide-react' +import Link from 'next/link' +import { useRouter } from 'next/navigation' +import Loading from '@/components/Loading' + +export default function LoginPage() { + const [mobile, setMobile] = useState('') + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const router = useRouter() + const mobileRef = useRef(null) + + useEffect(() => { + const checkAutoFill = () => { + setTimeout(() => { + if (mobileRef.current) { + const filledMobile = mobileRef.current.value + if (filledMobile && filledMobile !== mobile) { + setMobile(filledMobile) + } + } + }, 100) + } + + checkAutoFill() + window.addEventListener('load', checkAutoFill) + + return () => { + window.removeEventListener('load', checkAutoFill) + } + }, [mobile]) + + const handleInputChange = (e: React.FormEvent) => { + const value = e.currentTarget.value + // Only allow digits + const digitsOnly = value.replace(/\D/g, '') + setMobile(digitsOnly) + setError(null) + } + + const normalizeMobile = (mobile: string): string => { + const digitsOnly = mobile.replace(/\D/g, '') + if (digitsOnly.startsWith('9') && digitsOnly.length === 10) { + return '0' + digitsOnly + } + if (digitsOnly.startsWith('0098') && digitsOnly.length === 13) { + return '0' + digitsOnly.substring(3) + } + if (digitsOnly.startsWith('98') && digitsOnly.length === 12) { + return '0' + digitsOnly.substring(2) + } + return digitsOnly + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError(null) + + const normalizedMobile = normalizeMobile(mobile) + + if (!normalizedMobile || normalizedMobile.length !== 11 || !normalizedMobile.startsWith('09')) { + setError('لطفاً شماره موبایل معتبر وارد کنید (مثال: 09123456789)') + return + } + + setLoading(true) + + try { + const result = await api.sendCode({ mobile: normalizedMobile }) + + if (result.success) { + // Redirect to verify page with mobile number + router.push(`/verify-code?mobile=${encodeURIComponent(normalizedMobile)}`) + } else { + setError(result.message || 'خطا در ارسال کد') + } + } catch (error: any) { + console.error('Error sending code:', error) + setError(error.message || 'خطا در ارتباط با سرور') + } finally { + setLoading(false) + } + } + + const canSubmit = mobile.length >= 10 + + if (loading) { + return + } + + return ( +
+
+
+
+
+ +
+

+ ورود با شماره موبایل +

+

+ شماره موبایل خود را وارد کنید تا کد فعال‌سازی برای شما ارسال شود +

+
+ +
+
+ + +
+ + {error && ( +
+ {error} +
+ )} + + +
+
+ +
+
+ ) +} + diff --git a/src/app/page.tsx b/src/app/page.tsx index f306332..0a0a5aa 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,90 +1,175 @@ -import { Home as HomeIcon, Settings, Calendar, BarChart3, Leaf } from 'lucide-react' -import Link from 'next/link' +"use client" +import { useEffect, useState, useRef } from 'react' +import { useRouter } from 'next/navigation' +import { api } from '@/lib/api' +import { Smartphone, ArrowLeft } from 'lucide-react' +import Loading from '@/components/Loading' export default function Home() { - const menuItems = [ - { - title: 'انتخاب دستگاه', - description: 'اتصال به دستگاه گلخانه', - href: '/devices', - icon: Settings, - color: 'from-blue-500 to-blue-600', - iconColor: 'text-blue-500' - }, - { - title: 'تقویم', - description: 'مشاهده داده‌های ماهانه', - href: '/calendar?deviceId=1', - icon: Calendar, - color: 'from-green-500 to-green-600', - iconColor: 'text-green-500' - }, - { - title: 'جزئیات روز', - description: 'داده‌های روزانه', - href: '/day-details?deviceId=1&year=1403&month=1', - icon: BarChart3, - color: 'from-purple-500 to-purple-600', - iconColor: 'text-purple-500' - }, - { - title: 'داده‌های تله‌متری', - description: 'مشاهده داده‌های لحظه‌ای', - href: '/telemetry?deviceId=1', - icon: Leaf, - color: 'from-orange-500 to-orange-600', - iconColor: 'text-orange-500' + const router = useRouter() + const [mobile, setMobile] = useState('') + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const mobileRef = useRef(null) + + useEffect(() => { + // Check if user is logged in + const token = localStorage.getItem('authToken') + const userStr = localStorage.getItem('user') + + if (token && userStr) { + try { + const user = JSON.parse(userStr) + // Redirect to devices check + router.push('/devices') + } catch { + // Invalid user data, go to login + } } - ] + }, [router]) + + useEffect(() => { + const checkAutoFill = () => { + setTimeout(() => { + if (mobileRef.current) { + const filledMobile = mobileRef.current.value + if (filledMobile && filledMobile !== mobile) { + setMobile(filledMobile) + } + } + }, 100) + } + + checkAutoFill() + window.addEventListener('load', checkAutoFill) + + return () => { + window.removeEventListener('load', checkAutoFill) + } + }, [mobile]) + + const handleInputChange = (e: React.FormEvent) => { + const value = e.currentTarget.value + // Only allow digits + const digitsOnly = value.replace(/\D/g, '') + setMobile(digitsOnly) + setError(null) + } + + const normalizeMobile = (mobile: string): string => { + const digitsOnly = mobile.replace(/\D/g, '') + if (digitsOnly.startsWith('9') && digitsOnly.length === 10) { + return '0' + digitsOnly + } + if (digitsOnly.startsWith('0098') && digitsOnly.length === 13) { + return '0' + digitsOnly.substring(3) + } + if (digitsOnly.startsWith('98') && digitsOnly.length === 12) { + return '0' + digitsOnly.substring(2) + } + return digitsOnly + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError(null) + + const normalizedMobile = normalizeMobile(mobile) + + if (!normalizedMobile || normalizedMobile.length !== 11 || !normalizedMobile.startsWith('09')) { + setError('لطفاً شماره موبایل معتبر وارد کنید (مثال: 09123456789)') + return + } + + setLoading(true) + + try { + const result = await api.sendCode({ mobile: normalizedMobile }) + + if (result.success) { + // Redirect to verify page with mobile number + router.push(`/verify-code?mobile=${encodeURIComponent(normalizedMobile)}`) + } else { + setError(result.message || 'خطا در ارسال کد') + } + } catch (error: any) { + console.error('Error sending code:', error) + setError(error.message || 'خطا در ارتباط با سرور') + } finally { + setLoading(false) + } + } + + const canSubmit = mobile.length >= 10 + + if (loading) { + return + } return ( -
-
- {/* Header */} -
-
- +
+
+
+
+
+ +
+

+ ورود با شماره موبایل +

+

+ شماره موبایل خود را وارد کنید تا کد فعال‌سازی برای شما ارسال شود +

-

GreenHome

-

مدیریت هوشمند گلخانه

-
- {/* Menu Grid */} -
- {menuItems.map((item) => { - const Icon = item.icon - return ( - -
-
-
- -
-
-

- {item.title} -

-

{item.description}

-
-
-
-
- - ) - })} -
+
+
+ + +
- {/* Footer Info */} -
-

- سیستم مدیریت و مانیتورینگ گلخانه هوشمند -

+ {error && ( +
+ {error} +
+ )} + + +
-
+
) } diff --git a/src/app/verify-code/page.tsx b/src/app/verify-code/page.tsx new file mode 100644 index 0000000..634bf56 --- /dev/null +++ b/src/app/verify-code/page.tsx @@ -0,0 +1,307 @@ +"use client" +import { useState, useEffect, useRef } from 'react' +import { useSearchParams, useRouter } from 'next/navigation' +import { api } from '@/lib/api' +import { Shield, ArrowRight, ArrowLeft, RotateCcw } from 'lucide-react' +import Link from 'next/link' +import Loading from '@/components/Loading' + +export default function VerifyCodePage() { + const searchParams = useSearchParams() + const router = useRouter() + const mobile = searchParams.get('mobile') || '' + + const [code, setCode] = useState(['', '', '', '']) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [resendCooldown, setResendCooldown] = useState(120) + const [canResend, setCanResend] = useState(false) + const inputRefs = useRef<(HTMLInputElement | null)[]>([]) + + useEffect(() => { + if (!mobile || mobile.length !== 11) { + router.push('/login') + return + } + + // Check resend cooldown + checkResendStatus() + const interval = setInterval(() => { + setResendCooldown(prev => { + if (prev <= 1) { + setCanResend(true) + return 0 + } + return prev - 1 + }) + }, 1000) + + return () => clearInterval(interval) + }, [mobile, router]) + + const checkResendStatus = async () => { + try { + const result = await api.canResend(mobile) + if (result.canResend) { + setCanResend(true) + setResendCooldown(0) + } else { + setCanResend(false) + setResendCooldown(120) + } + } catch (error) { + console.error('Error checking resend status:', error) + } + } + + const handleCodeChange = (index: number, value: string) => { + if (!/^\d*$/.test(value)) return + + const newCode = [...code] + newCode[index] = value.slice(-1) + setCode(newCode) + setError(null) + + // Auto-focus next input + if (value && index < 3) { + inputRefs.current[index + 1]?.focus() + } + + // Auto-submit when all fields are filled + if (newCode.every(c => c !== '') && newCode.join('').length === 4) { + handleVerify(newCode.join('')) + } + } + + const handleKeyDown = (index: number, e: React.KeyboardEvent) => { + if (e.key === 'Backspace' && !code[index] && index > 0) { + inputRefs.current[index - 1]?.focus() + } + } + + const handlePaste = (e: React.ClipboardEvent) => { + e.preventDefault() + const pastedData = e.clipboardData.getData('text').replace(/\D/g, '').slice(0, 4) + if (pastedData.length === 4) { + const newCode = pastedData.split('') + setCode(newCode) + inputRefs.current[3]?.focus() + handleVerify(pastedData) + } + } + + const handleVerify = async (codeToVerify?: string) => { + const codeValue = codeToVerify || code.join('') + + if (codeValue.length !== 4) { + setError('لطفاً کد ۴ رقمی را کامل وارد کنید') + return + } + + setLoading(true) + setError(null) + + try { + const result = await api.verifyCode({ mobile, code: codeValue }) + + if (result.success && result.token && result.user) { + // Store token in localStorage + localStorage.setItem('authToken', result.token) + localStorage.setItem('user', JSON.stringify(result.user)) + + // Check user devices + try { + const devicesResult = await api.getDevicesFiltered({ + userId: result.user.id, + page: 1, + pageSize: 10 + }) + + if (devicesResult.totalCount === 0) { + // No devices - show error and logout + setError('شما هیچ دستگاهی ندارید. لطفاً با پشتیبانی تماس بگیرید.') + localStorage.removeItem('authToken') + localStorage.removeItem('user') + return + } else if (devicesResult.totalCount === 1) { + // Single device - redirect to calendar + router.push(`/calendar?deviceId=${devicesResult.items[0].id}`) + } else { + // Multiple devices - redirect to device list + router.push('/devices') + } + } catch (deviceError) { + console.error('Error fetching devices:', deviceError) + // Still redirect to devices page to handle error there + router.push('/devices') + } + } else { + setError(result.message || 'کد وارد شده نادرست است') + // Clear code inputs + setCode(['', '', '', '']) + inputRefs.current[0]?.focus() + } + } catch (error: any) { + console.error('Error verifying code:', error) + setError(error.message || 'خطا در ارتباط با سرور') + setCode(['', '', '', '']) + inputRefs.current[0]?.focus() + } finally { + setLoading(false) + } + } + + const handleResend = async () => { + if (!canResend) return + + setLoading(true) + setError(null) + setCanResend(false) + setResendCooldown(120) + + try { + const result = await api.sendCode({ mobile }) + + if (result.success) { + setResendCooldown(result.resendAfterSeconds || 120) + // Clear code inputs + setCode(['', '', '', '']) + inputRefs.current[0]?.focus() + } else { + setError(result.message || 'خطا در ارسال مجدد کد') + setCanResend(true) + } + } catch (error: any) { + console.error('Error resending code:', error) + setError(error.message || 'خطا در ارتباط با سرور') + setCanResend(true) + } finally { + setLoading(false) + } + } + + const formatMobile = (mobile: string) => { + if (mobile.length === 11) { + //return `${mobile.slice(0, 4)}${mobile.slice(4, 7)} ${mobile.slice(7)}` + return mobile; + } + return mobile + } + + if (loading && code.join('').length !== 4) { + return + } + + return ( +
+
+
+
+
+ +
+

+ تایید کد فعال‌سازی +

+

+ کد ارسال شده به شماره +

+

+ {formatMobile(mobile)} +

+

+ را وارد کنید +

+
+ +
{ e.preventDefault(); handleVerify(); }} className="space-y-5"> +
+ {code.map((digit, index) => ( + { inputRefs.current[index] = el }} + type="text" + inputMode="numeric" + maxLength={1} + value={digit} + onChange={(e) => handleCodeChange(index, e.target.value)} + onKeyDown={(e) => handleKeyDown(index, e)} + onPaste={index === 0 ? handlePaste : undefined} + className="w-14 h-14 text-center text-2xl font-bold rounded-xl border-2 border-gray-200 bg-gray-50 focus:border-green-500 focus:bg-white focus:outline-none focus:ring-2 focus:ring-green-500/20 transition-all duration-200" + disabled={loading} + /> + ))} +
+ + {error && ( +
+ {error} +
+ )} + + + +
+ + + + + تغییر شماره موبایل + +
+
+
+ +
+ + + بازگشت به صفحه اصلی + +
+
+
+ ) +} + diff --git a/src/lib/api.ts b/src/lib/api.ts index 0bac343..c1db620 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,8 +1,12 @@ +"use client" + export type DeviceDto = { id: number deviceName: string - owner: string - mobile: string + userId: number + userName: string + userFamily: string + userMobile: string location: string neshanLocation: string } @@ -40,6 +44,33 @@ export type DeviceSettingsDto = { updatedAt: string } +export type SendCodeRequest = { + mobile: string +} + +export type SendCodeResponse = { + success: boolean + message?: string + resendAfterSeconds: number +} + +export type VerifyCodeRequest = { + mobile: string + code: string +} + +export type VerifyCodeResponse = { + success: boolean + message?: string + token?: string + user?: { + id: number + mobile: string + name: string + family: string + } +} + export type PagedResult = { items: T[] totalCount: number @@ -48,11 +79,21 @@ export type PagedResult = { } const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? 'https://ghback.nabaksoft.ir' - async function http(url: string, init?: RequestInit): Promise { const res = await fetch(url, { ...init, headers: { 'Content-Type': 'application/json', ...(init?.headers || {}) } }) if (!res.ok) { - throw new Error(`HTTP ${res.status}`) + let errorMessage = `HTTP ${res.status}` + try { + const errorData = await res.json() + if (errorData.message) { + errorMessage = errorData.message + } + } catch { + // Ignore JSON parse errors + } + const error: any = new Error(errorMessage) + error.status = res.status + throw error } return res.json() as Promise } @@ -83,5 +124,20 @@ export const api = { // Device Settings getDeviceSettings: (deviceId: number) => http(`${API_BASE}/api/devicesettings/${deviceId}`), createDeviceSettings: (dto: DeviceSettingsDto) => http(`${API_BASE}/api/devicesettings`, { method: 'POST', body: JSON.stringify(dto) }), - updateDeviceSettings: (dto: DeviceSettingsDto) => http(`${API_BASE}/api/devicesettings`, { method: 'PUT', body: JSON.stringify(dto) }) + updateDeviceSettings: (dto: DeviceSettingsDto) => http(`${API_BASE}/api/devicesettings`, { method: 'PUT', body: JSON.stringify(dto) }), + + // Authentication + sendCode: (request: SendCodeRequest) => http(`${API_BASE}/api/auth/send-code`, { method: 'POST', body: JSON.stringify(request) }), + verifyCode: (request: VerifyCodeRequest) => http(`${API_BASE}/api/auth/verify-code`, { method: 'POST', body: JSON.stringify(request) }), + canResend: (mobile: string) => http<{ canResend: boolean }>(`${API_BASE}/api/auth/can-resend?mobile=${encodeURIComponent(mobile)}`), + + // User Devices + getUserDevices: (userId: number) => http(`${API_BASE}/api/devices/user/${userId}`), + getDevicesFiltered: (q: { userId: number; search?: string; page?: number; pageSize?: number }) => { + const params = new URLSearchParams({ userId: String(q.userId) }) + if (q.search) params.set('search', q.search) + if (q.page) params.set('page', String(q.page)) + if (q.pageSize) params.set('pageSize', String(q.pageSize)) + return http>(`${API_BASE}/api/devices/filtered?${params.toString()}`) + } }