Browse Source

feat: Add universal search to devices frontend

Frontend changes:
- Added search input to superadmin/DevicesView
- Debounce 300ms after user stops typing
- Clear search button (×)
- Updated devices API to accept params
- Added i18n translations for search placeholder (en/ru)

Features:
- Real-time filtering as user types
- Searches: MAC, simple_id, organization name/email
- Clean UX with clear button

Next: Add search to client devices view

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
root 1 month ago
parent
commit
ce0976a47e

+ 4 - 4
frontend/src/api/devices.js

@@ -2,8 +2,8 @@ import client from './client'
 
 export default {
   // Superadmin endpoints
-  async getAllSuperadmin() {
-    const { data } = await client.get('/superadmin/devices')
+  async getAllSuperadmin(params = {}) {
+    const { data } = await client.get('/superadmin/devices', { params })
     return data.devices || []
   },
 
@@ -23,8 +23,8 @@ export default {
   },
 
   // Client endpoints
-  async getAllClient() {
-    const { data } = await client.get('/client/devices')
+  async getAllClient(params = {}) {
+    const { data } = await client.get('/client/devices', { params })
     return data.devices || []
   },
 

+ 2 - 0
frontend/src/i18n/index.js

@@ -61,6 +61,7 @@ const messages = {
       macAddress: 'MAC Address',
       organization: 'Organization',
       lastSeen: 'Last Seen',
+      searchPlaceholder: 'Search by MAC, ID, organization...',
       manageAction: 'Manage Devices',
       manageDesc: 'View and assign devices',
       online: 'Online',
@@ -153,6 +154,7 @@ const messages = {
       macAddress: 'MAC адрес',
       organization: 'Организация',
       lastSeen: 'Последняя активность',
+      searchPlaceholder: 'Поиск по MAC, ID, организации...',
       manageAction: 'Управление устройствами',
       manageDesc: 'Просмотр и назначение устройств',
       online: 'Онлайн',

+ 40 - 1
frontend/src/views/superadmin/DevicesView.vue

@@ -8,6 +8,18 @@
     </div>
 
     <div class="content">
+      <!-- Universal Search -->
+      <div class="search-box">
+        <input
+          v-model="searchQuery"
+          type="text"
+          :placeholder="$t('devices.searchPlaceholder')"
+          class="search-input"
+          @input="onSearch"
+        />
+        <span v-if="searchQuery" class="search-clear" @click="clearSearch">×</span>
+      </div>
+
       <div v-if="loading" class="loading">{{ $t('common.loading') }}</div>
 
       <div v-else-if="error" class="error">{{ error }}</div>
@@ -125,6 +137,8 @@ const editingDevice = ref(null)
 const deviceToDelete = ref(null)
 const saving = ref(false)
 const deleting = ref(false)
+const searchQuery = ref('')
+let searchDebounceTimer = null
 
 const form = ref({
   organization_id: null,
@@ -135,7 +149,11 @@ async function loadDevices() {
   loading.value = true
   error.value = null
   try {
-    devices.value = await devicesApi.getAllSuperadmin()
+    const params = {}
+    if (searchQuery.value && searchQuery.value.length >= 2) {
+      params.search = searchQuery.value
+    }
+    devices.value = await devicesApi.getAllSuperadmin(params)
   } catch (err) {
     error.value = err.response?.data?.detail || 'Failed to load devices'
   } finally {
@@ -143,6 +161,19 @@ async function loadDevices() {
   }
 }
 
+function onSearch() {
+  // Debounce search - wait 300ms after user stops typing
+  clearTimeout(searchDebounceTimer)
+  searchDebounceTimer = setTimeout(() => {
+    loadDevices()
+  }, 300)
+}
+
+function clearSearch() {
+  searchQuery.value = ''
+  loadDevices()
+}
+
 async function loadOrganizations() {
   try {
     organizations.value = await organizationsApi.getAll()
@@ -220,6 +251,14 @@ onMounted(() => {
 .page-header h1 { font-size: 32px; font-weight: 700; color: #1a202c; margin-bottom: 8px; }
 .page-header p { color: #718096; font-size: 16px; }
 .content { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); }
+
+/* Universal Search */
+.search-box { position: relative; margin-bottom: 20px; }
+.search-input { width: 100%; padding: 12px 40px 12px 16px; border: 1px solid #e2e8f0; border-radius: 8px; font-size: 14px; transition: border-color 0.2s; }
+.search-input:focus { outline: none; border-color: #667eea; }
+.search-input::placeholder { color: #a0aec0; }
+.search-clear { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); font-size: 24px; color: #a0aec0; cursor: pointer; line-height: 1; padding: 0 4px; }
+.search-clear:hover { color: #718096; }
 .loading, .error, .empty { text-align: center; padding: 40px; color: #718096; }
 .error { color: #e53e3e; }
 .data-table { width: 100%; border-collapse: collapse; }