Explorar o código

feat(water): complete device T6 UI contract

Kazerin hai 1 día
pai
achega
e8aa40297c

+ 82 - 8
src/utils/waterSupplyUiModel.js

@@ -16,21 +16,93 @@ const workOrderStatusLabels = {
   8: '已取消'
 }
 
+const deviceCurrentStatusLabels = {
+  1: '在用',
+  2: '闲置',
+  3: '维修',
+  4: '报废',
+  5: '待入库'
+}
+
+const onlineStatusLabels = {
+  1: '在线',
+  0: '离线'
+}
+
 export function normalizeDeviceRow(row = {}) {
-  const alarmStatus = Number(row.alarmStatus ?? row.status ?? 0)
+  const alarmStatus = Number(row.alarmStatus)
+  const currentStatus = Number(row.currentStatus)
+  const onlineStatus = Number(row.onlineStatus)
   const alarmLevel = Number(row.alertLevel ?? row.alarmLevel ?? 0)
-  const relatedOrderNo = row.latestOrderNo ?? row.orderNo ?? row.latestMaintainId ?? '-'
-  const rawOrderStatus = row.latestMaintainStatus ?? row.orderStatus ?? row.statusText
+  const relatedOrderNo = row.relatedWorkOrderNo
+    ?? row.latestOrderNo
+    ?? row.orderNo
+    ?? row.latestMaintainId
+    ?? '-'
+  const rawOrderStatus = row.relatedWorkOrderStatus
+    ?? row.latestMaintainStatus
+    ?? row.orderStatus
+    ?? row.statusText
+  const currentStatusText = deviceCurrentStatusLabels[currentStatus]
+    ?? (currentStatus ? `状态${currentStatus}` : '未知')
+  const onlineStatusText = onlineStatusLabels[onlineStatus]
+    ?? (Number.isFinite(onlineStatus) && onlineStatus !== 0 ? '在线' : '离线')
+  const alarmStatusText = alarmStatus === 1 ? '报警' : '正常'
 
+  const workOrderStatus = normalizeWorkOrderStatus(rawOrderStatus)
   return {
     ...row,
-    deviceStatusText: alarmStatus === 1 ? '报警' : '正常',
+    deviceStatusText: currentStatusText,
+    currentStatusText,
+    onlineStatusText,
+    alarmStatusText,
+    abnormalReasonText: row.abnormalReason
+      || (alarmStatus === 1 ? '设备报警' : currentStatus === 3 ? '设备维修中' : '-'),
     alarmLevelText: alarmLevelLabels[alarmLevel] ?? (alarmLevel ? `${alarmLevel}级报警` : '未分级'),
     relatedOrderNo,
-    workOrderStatusText: typeof rawOrderStatus === 'number'
-      ? (workOrderStatusLabels[rawOrderStatus] ?? '未知')
-      : (rawOrderStatus || '暂无工单')
+    relatedWorkOrderPerson: row.relatedWorkOrderPerson ?? '-',
+    workOrderStatusCode: workOrderStatus.code,
+    workOrderTerminal: workOrderStatus.terminal,
+    workOrderStatusText: workOrderStatus.text
+  }
+}
+
+export function normalizeWorkOrderStatus(value) {
+  if (value === null || value === undefined || value === '') {
+    return { code: null, text: '暂无工单', terminal: false }
   }
+  const numeric = typeof value === 'number' || /^\s*\d+\s*$/.test(String(value))
+  if (numeric) {
+    const code = Number(value)
+    return {
+      code: Number.isFinite(code) ? code : null,
+      text: workOrderStatusLabels[code] ?? '未知',
+      terminal: code >= 6
+    }
+  }
+  const text = String(value)
+  const code = Object.entries(workOrderStatusLabels).find(([, label]) => label === text)?.[0]
+  return { code: code ? Number(code) : null, text: text || '暂无工单', terminal: code ? Number(code) >= 6 : false }
+}
+
+export function canDispatchDevice(row = {}) {
+  const orderNo = row.relatedOrderNo ?? row.latestOrderNo ?? row.orderNo
+  if (!orderNo || orderNo === '-') return true
+  return normalizeWorkOrderStatus(row.relatedWorkOrderStatus ?? row.latestMaintainStatus ?? row.orderStatus).terminal
+}
+
+export function preserveDevicePageOnError(page) {
+  return { page, error: '设备数据加载失败,请点击重试' }
+}
+
+export function normalizeDeviceGisPoints(response) {
+  const data = response?.data ?? response
+  const points = Array.isArray(data) ? data : (data?.records || data?.rows || data?.points || [])
+  return points.map(point => ({
+    ...point,
+    _longitude: Number(point.longitude ?? point.lng),
+    _latitude: Number(point.latitude ?? point.lat)
+  })).filter(point => Number.isFinite(point._longitude) && Number.isFinite(point._latitude))
 }
 
 export function buildMaintainOrderPayload(device = {}, form = {}) {
@@ -49,7 +121,9 @@ export function buildMaintainOrderPayload(device = {}, form = {}) {
 }
 
 export function getDeviceOrderEndpoint(view) {
-  return view === 'dispatch' ? '/waterSupply/device/abnormal/order' : null
+  return view === 'dispatch' || view === 'abnormal'
+    ? '/waterSupply/device/abnormal/order'
+    : null
 }
 
 function normalizeThresholdValue(value) {

+ 37 - 14
src/views/subSystem/waterSupply/components/WaterDevicePage.vue

@@ -20,19 +20,25 @@
 
     <el-card shadow="never" class="water-panel">
       <div class="card-title"><span><el-icon><Monitor /></el-icon> {{ title }}清单</span><el-button link type="primary" @click="exportData">导出</el-button></div>
+      <el-alert v-if="loadError" :title="loadError" type="error" show-icon :closable="false" class="load-error"><el-button size="small" type="danger" plain @click="loadData">重试</el-button></el-alert>
+      <el-empty v-if="view === 'gis' && !loading && !loadError && !page.records.length" description="暂无设备 GIS 点位数据" />
       <el-table :data="page.records" border stripe v-loading="loading" row-key="equipmentId">
         <el-table-column type="index" label="#" width="55" /><el-table-column prop="equipmentCode" label="设备编码" min-width="130" /><el-table-column prop="equipmentName" label="设备名称" min-width="150" /><el-table-column prop="equipmentTypeName" label="设备类型" min-width="120" /><el-table-column prop="equipmentLocation" label="安装位置" min-width="180" show-overflow-tooltip />
-        <el-table-column v-if="view !== 'maintain'" label="设备状态" width="100"><template #default="{ row }"><el-tag :type="Number(row.alarmStatus) === 1 ? 'danger' : 'success'">{{ row.deviceStatusText }}</el-tag></template></el-table-column>
+        <el-table-column v-if="view !== 'maintain'" label="设备状态" width="100"><template #default="{ row }"><el-tag :type="Number(row.currentStatus) === 3 ? 'danger' : 'success'">{{ row.currentStatusText }}</el-tag></template></el-table-column>
+        <el-table-column v-if="view !== 'maintain'" label="在线状态" width="100"><template #default="{ row }"><el-tag :type="Number(row.onlineStatus) === 1 ? 'success' : 'info'">{{ row.onlineStatusText }}</el-tag></template></el-table-column>
+        <el-table-column v-if="view !== 'maintain'" label="报警状态" width="100"><template #default="{ row }"><el-tag :type="Number(row.alarmStatus) === 1 ? 'danger' : 'success'">{{ row.alarmStatusText }}</el-tag></template></el-table-column>
         <el-table-column v-if="view !== 'maintain'" label="报警等级" width="110"><template #default="{ row }"><el-tag :type="alarmLevelType(row.alertLevel)">{{ row.alarmLevelText }}</el-tag></template></el-table-column>
-        <el-table-column label="关联维修单" min-width="190" show-overflow-tooltip><template #default="{ row }">{{ view === 'maintain' ? (row.orderNo || '-') : row.relatedOrderNo }}</template></el-table-column>
-        <el-table-column label="维修状态" min-width="110"><template #default="{ row }">{{ view === 'maintain' ? (row.status || '暂无') : row.workOrderStatusText }}</template></el-table-column>
-        <el-table-column label="操作" width="190" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="selectPoint(row)">详情</el-button><el-button v-if="view === 'dispatch'" link type="warning" @click="openOrder(row)">维修派单</el-button><el-button v-if="view === 'maintain'" link type="danger" @click="removeRecord(row)">删除</el-button></template></el-table-column>
+        <el-table-column v-if="view === 'abnormal' || view === 'dispatch'" label="异常原因" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ row.abnormalReasonText || '-' }}</template></el-table-column>
+        <el-table-column label="关联工单" min-width="190" show-overflow-tooltip><template #default="{ row }">{{ view === 'maintain' ? (row.orderNo || '-') : row.relatedOrderNo }}</template></el-table-column>
+        <el-table-column label="工单状态" min-width="110"><template #default="{ row }"><el-tag v-if="view !== 'maintain' && row.workOrderStatusText !== '暂无工单'" :type="workOrderStatusType(row.workOrderStatusCode)">{{ row.workOrderStatusText }}</el-tag><span v-else>{{ view === 'maintain' ? (row.status || '暂无') : row.workOrderStatusText }}</span></template></el-table-column>
+        <el-table-column v-if="view === 'abnormal' || view === 'dispatch'" label="接单责任人" min-width="110"><template #default="{ row }">{{ view === 'maintain' ? (row.maintainPerson || '-') : (row.relatedWorkOrderPerson || '-') }}</template></el-table-column>
+        <el-table-column label="操作" width="190" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="selectPoint(row)">详情</el-button><el-button v-if="canDispatch(row)" link type="warning" @click="openOrder(row)">维修派单</el-button><el-button v-if="view === 'maintain'" link type="danger" @click="removeRecord(row)">删除</el-button></template></el-table-column>
       </el-table>
       <el-pagination v-model:current-page="query.pageNum" v-model:page-size="query.pageSize" :total="page.total" layout="total, sizes, prev, pager, next" :page-sizes="[10, 20, 50]" @change="loadData" />
     </el-card>
 
     <el-drawer v-model="detailVisible" class="detail-drawer device-detail-drawer" title="设备详情" direction="rtl" size="680px" append-to-body>
-      <div class="detail-summary"><span class="detail-name">{{ detail.equipmentName || '-' }}</span><el-tag :type="Number(detail.alarmStatus) === 1 ? 'danger' : 'success'">{{ detail.deviceStatusText || '-' }}</el-tag></div>
+      <div class="detail-summary"><span class="detail-name">{{ detail.equipmentName || '-' }}</span><el-tag :type="Number(detail.currentStatus) === 3 ? 'danger' : 'success'">{{ detail.currentStatusText || '-' }}</el-tag><el-tag :type="Number(detail.alarmStatus) === 1 ? 'danger' : 'success'">{{ detail.alarmStatusText || '-' }}</el-tag><el-tag v-if="detail.workOrderStatusText && detail.workOrderStatusText !== '暂无工单'" :type="workOrderStatusType(detail.workOrderStatusCode)">{{ detail.workOrderStatusText }}</el-tag></div>
       <el-descriptions :column="2" border>
         <el-descriptions-item v-for="item in detailFields" :key="item.label" :label="item.label">{{ item.value }}</el-descriptions-item>
       </el-descriptions>
@@ -47,25 +53,42 @@ import { computed, onMounted, ref } from 'vue'
 import { ElMessage } from 'element-plus'
 import { Monitor, Refresh } from '@element-plus/icons-vue'
 import { listUser } from '@/api/system/user'
-import { createAbnormalOrder, exportAbnormalDevices, exportDeviceLedger, listAbnormalDevices, listDeviceLedger, listMaintainRecords, deleteMaintainRecord } from '@/api/pipeNetwork/waterSupply'
-import { normalizePage } from '@/utils/waterSupplyModel'
-import { buildMaintainOrderPayload, normalizeDeviceRow } from '@/utils/waterSupplyUiModel'
+import { createAbnormalOrder, exportAbnormalDevices, exportDeviceLedger, listAbnormalDevices, listDeviceLedger, listDeviceGisPoints, listMaintainRecords, deleteMaintainRecord } from '@/api/pipeNetwork/waterSupply'
+import { normalizePage, readMonitoringExportError } from '@/utils/waterSupplyModel'
+import { buildMaintainOrderPayload, normalizeDeviceRow, canDispatchDevice, normalizeDeviceGisPoints, preserveDevicePageOnError } from '@/utils/waterSupplyUiModel'
+import { saveAs } from 'file-saver'
 
 const props = defineProps({ title: { type: String, required: true }, subtitle: { type: String, default: '设备信息、运行状态与维护过程管理' }, view: { type: String, default: 'ledger' } })
 const { title, subtitle, view } = props
 const query = ref({ pageNum: 1, pageSize: 10, equipmentName: '', equipmentCode: '', alarmStatus: undefined, alertLevel: undefined })
-const page = ref({ records: [], total: 0 }); const loading = ref(false); const detailVisible = ref(false); const detail = ref({}); const dispatchVisible = ref(false); const dispatching = ref(false); const dispatchDevice = ref({}); const dispatchForm = ref({ description: '', receiveUserId: undefined }); const users = ref([]); const userLoading = ref(false)
+const page = ref({ records: [], total: 0 }); const loading = ref(false); const loadError = ref(''); const detailVisible = ref(false); const detail = ref({}); const dispatchVisible = ref(false); const dispatching = ref(false); const dispatchDevice = ref({}); const dispatchForm = ref({ description: '', receiveUserId: undefined }); const users = ref([]); const userLoading = ref(false)
 const levels = [{ label: '一级', value: 1 }, { label: '二级', value: 2 }, { label: '三级', value: 3 }, { label: '四级', value: 4 }]; const pageIcon = Monitor; const alarmLevelType = level => ({ 1: 'danger', 2: 'warning', 3: 'primary', 4: 'success' }[Number(level)] || 'info')
-const detailFields = computed(() => [['设备编码', detail.value.equipmentCode], ['设备类型', detail.value.equipmentTypeName || detail.value.typeName], ['安装位置', detail.value.equipmentLocation], ['设备状态', detail.value.deviceStatusText], ['报警等级', detail.value.alarmLevelText], ['在线状态', Number(detail.value.onlineStatus) === 1 ? '在线' : '离线'], ['关联维修单', detail.value.relatedOrderNo || detail.value.orderNo], ['维修状态', detail.value.workOrderStatusText || detail.value.status], ['状态更新时间', detail.value.statusUpdateTime]].map(([label, value]) => ({ label, value: value ?? '-' })))
+function canDispatch(row) {
+  if (view !== 'dispatch' && view !== 'abnormal') return false
+  return canDispatchDevice(row)
+}
+function workOrderStatusType(code) { return ({ 1: 'info', 2: 'warning', 3: 'primary', 4: 'warning', 5: 'danger', 6: 'success', 7: 'info', 8: 'info' })[Number(code)] || 'info' }
+const detailFields = computed(() => [['设备编码', detail.value.equipmentCode], ['设备类型', detail.value.equipmentTypeName || detail.value.typeName], ['安装位置', detail.value.equipmentLocation], ['设备状态', detail.value.currentStatusText], ['在线状态', detail.value.onlineStatusText], ['报警状态', detail.value.alarmStatusText], ['报警等级', detail.value.alarmLevelText], ['异常原因', detail.value.abnormalReasonText], ['关联工单', detail.value.relatedOrderNo || detail.value.orderNo], ['工单状态', detail.value.workOrderStatusText || detail.value.status], ['接单责任人', detail.value.relatedWorkOrderPerson || detail.value.maintainPerson], ['状态更新时间', detail.value.statusUpdateTime]].map(([label, value]) => ({ label, value: value ?? '-' })))
 
-async function loadData() { loading.value = true; try { let response; if (view === 'maintain') response = await listMaintainRecords({ ...query.value }); else if (view === 'dispatch' || view === 'abnormal') response = await listAbnormalDevices({ ...query.value }); else response = await listDeviceLedger({ ...query.value }); const normalized = normalizePage(response); page.value = { ...normalized, records: normalized.records.map(normalizeDeviceRow) } } catch (error) { page.value = { records: [], total: 0 }; console.warn(`[${title}] 数据加载失败`, error) } finally { loading.value = false } }
+async function loadData() { loading.value = true; loadError.value = ''; try { if (view === 'gis') { const response = await listDeviceGisPoints({ ...query.value }); const records = normalizeDeviceGisPoints(response).map(normalizeDeviceRow); page.value = { records, total: records.length, current: 1, size: records.length || 10 }; return } let response; if (view === 'maintain') response = await listMaintainRecords({ ...query.value }); else if (view === 'dispatch' || view === 'abnormal') response = await listAbnormalDevices({ ...query.value }); else response = await listDeviceLedger({ ...query.value }); const normalized = normalizePage(response); page.value = { ...normalized, records: normalized.records.map(normalizeDeviceRow) } } catch (error) { const retained = preserveDevicePageOnError(page.value); page.value = retained.page; loadError.value = retained.error; console.warn(`[${title}] 数据加载失败`, error) } finally { loading.value = false } }
 function reset() { query.value = { pageNum: 1, pageSize: 10, equipmentName: '', equipmentCode: '', alarmStatus: undefined, alertLevel: undefined }; loadData() }
 function selectPoint(row) { detail.value = normalizeDeviceRow(row); detailVisible.value = true }
 async function loadUsers() { if (users.value.length) return; userLoading.value = true; try { const response = await listUser({ pageNum: 1, pageSize: 200 }); users.value = response.rows || response.data?.rows || [] } catch (error) { ElMessage.error('责任人列表加载失败') } finally { userLoading.value = false } }
-async function openOrder(row) { dispatchDevice.value = row; dispatchForm.value = { description: row.abnormalDescription || row.description || '', receiveUserId: undefined }; await loadUsers(); dispatchVisible.value = true }
-async function submitDispatch() { if (!dispatchForm.value.description?.trim()) return ElMessage.warning('请填写问题描述'); if (!dispatchForm.value.receiveUserId) return ElMessage.warning('请选择接单责任人'); dispatching.value = true; try { await createAbnormalOrder(buildMaintainOrderPayload(dispatchDevice.value, dispatchForm.value)); ElMessage.success('维修派单已提交'); dispatchVisible.value = false; await loadData() } catch (error) { ElMessage.error('派单失败,请检查权限或服务状态') } finally { dispatching.value = false } }
+async function openOrder(row) { dispatchDevice.value = row; dispatchForm.value = { description: row.abnormalReason || row.abnormalReasonText || row.description || '', receiveUserId: undefined }; await loadUsers(); dispatchVisible.value = true }
+async function submitDispatch() { if (!dispatchForm.value.description?.trim()) return ElMessage.warning('请填写问题描述'); if (!dispatchForm.value.receiveUserId) return ElMessage.warning('请选择接单责任人'); dispatching.value = true; try { const response = await createAbnormalOrder(buildMaintainOrderPayload(dispatchDevice.value, dispatchForm.value)); const summary = response?.data?.data || response?.data || response || {}; const orderNo = summary.orderNo || summary.orderId; ElMessage.success(orderNo ? `已派单,等待接单(${orderNo})` : '已派单,等待接单'); dispatchVisible.value = false; await loadData() } catch (error) { ElMessage.error(error?.response?.data?.msg || '派单失败,请检查权限或服务状态') } finally { dispatching.value = false } }
 async function removeRecord(row) { try { await deleteMaintainRecord(row.maintainId || row.id); ElMessage.success('记录已删除'); await loadData() } catch (error) { ElMessage.error('删除失败') } }
-async function exportData() { try { await (view === 'dispatch' || view === 'abnormal' ? exportAbnormalDevices : exportDeviceLedger)({ ...query.value }); ElMessage.success('导出请求已提交') } catch (error) { ElMessage.error('导出失败') } }
+async function exportData() {
+  try {
+    const response = await (view === 'dispatch' || view === 'abnormal' ? exportAbnormalDevices : exportDeviceLedger)({ ...query.value })
+    const errorMessage = await readMonitoringExportError(response)
+    if (errorMessage) throw new Error(errorMessage)
+    const blob = response instanceof Blob ? response : new Blob([response], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
+    saveAs(blob, `${title}_${new Date().toISOString().slice(0, 10)}.xlsx`)
+    ElMessage.success('导出成功')
+  } catch (error) {
+    ElMessage.error(error?.message || '导出失败,请稍后重试')
+  }
+}
 onMounted(loadData)
 </script>
 

+ 60 - 3
tests/waterSupplyUiModel.test.mjs

@@ -3,7 +3,11 @@ import assert from 'node:assert/strict'
 import {
   normalizeDeviceRow,
   buildMaintainOrderPayload,
-  getDeviceOrderEndpoint
+  getDeviceOrderEndpoint,
+  normalizeWorkOrderStatus,
+  canDispatchDevice,
+  preserveDevicePageOnError,
+  normalizeDeviceGisPoints
 } from '../src/utils/waterSupplyUiModel.js'
 
 test('normalizes alarm and related work-order fields for device pages', () => {
@@ -17,12 +21,36 @@ test('normalizes alarm and related work-order fields for device pages', () => {
     latestMaintainStatus: '处理中'
   })
 
-  assert.equal(row.deviceStatusText, '报警')
+  assert.equal(row.alarmStatusText, '报警')
   assert.equal(row.alarmLevelText, '二级报警')
   assert.equal(row.relatedOrderNo, 'WO202606101703482640')
   assert.equal(row.workOrderStatusText, '处理中')
 })
 
+test('keeps device operation, online, alarm, and work-order states distinct', () => {
+  const row = normalizeDeviceRow({
+    equipmentId: 'EQ-T6',
+    equipmentCode: 'CN-GS-202609020001',
+    equipmentName: 'T6压力监测点',
+    currentStatus: 3,
+    onlineStatus: 0,
+    alarmStatus: 1,
+    alertLevel: 1,
+    abnormalReason: '供水压力超限',
+    relatedWorkOrderNo: 'WO202609020001',
+    relatedWorkOrderStatus: 4,
+    relatedWorkOrderPerson: '张三'
+  })
+
+  assert.equal(row.currentStatusText, '维修')
+  assert.equal(row.onlineStatusText, '离线')
+  assert.equal(row.alarmStatusText, '报警')
+  assert.equal(row.abnormalReasonText, '供水压力超限')
+  assert.equal(row.relatedOrderNo, 'WO202609020001')
+  assert.equal(row.workOrderStatusText, '处理中')
+  assert.equal(row.relatedWorkOrderPerson, '张三')
+})
+
 test('builds a repair dispatch payload with the selected responsible user', () => {
   const payload = buildMaintainOrderPayload(
     { equipmentId: 'EQ-1', equipmentCode: 'P-001', equipmentName: '一号压力计' },
@@ -41,5 +69,34 @@ test('builds a repair dispatch payload with the selected responsible user', () =
 
 test('uses the abnormal-device order endpoint for device repair dispatch', () => {
   assert.equal(getDeviceOrderEndpoint('dispatch'), '/waterSupply/device/abnormal/order')
-  assert.equal(getDeviceOrderEndpoint('abnormal'), null)
+  assert.equal(getDeviceOrderEndpoint('abnormal'), '/waterSupply/device/abnormal/order')
+})
+
+test('maps numeric and numeric-string work-order statuses and marks terminal states', () => {
+  assert.deepEqual(normalizeWorkOrderStatus(4), { code: 4, text: '处理中', terminal: false })
+  assert.deepEqual(normalizeWorkOrderStatus('6'), { code: 6, text: '已结案', terminal: true })
+  assert.deepEqual(normalizeWorkOrderStatus('7'), { code: 7, text: '已驳回', terminal: true })
+  assert.deepEqual(normalizeWorkOrderStatus('暂无工单'), { code: null, text: '暂无工单', terminal: false })
+})
+
+test('allows dispatch when the latest order is terminal, including numeric-string status', () => {
+  assert.equal(canDispatchDevice({ relatedOrderNo: 'WO-6', relatedWorkOrderStatus: '6' }), true)
+  assert.equal(canDispatchDevice({ relatedOrderNo: 'WO-7', relatedWorkOrderStatus: 7 }), true)
+  assert.equal(canDispatchDevice({ relatedOrderNo: 'WO-4', relatedWorkOrderStatus: '4' }), false)
+  assert.equal(canDispatchDevice({ relatedOrderNo: '-' }), true)
+})
+
+test('retains existing page rows and total when a reload fails', () => {
+  const previous = { records: [{ equipmentId: 'EQ-1' }], total: 1 }
+  assert.deepEqual(preserveDevicePageOnError(previous), {
+    page: previous,
+    error: '设备数据加载失败,请点击重试'
+  })
+})
+
+test('normalizes GIS responses to point records without inventing coordinates', () => {
+  assert.deepEqual(normalizeDeviceGisPoints({ data: [{ equipmentId: 'EQ-1', longitude: '110.1', latitude: '28.2' }] }), [
+    { equipmentId: 'EQ-1', longitude: '110.1', latitude: '28.2', _longitude: 110.1, _latitude: 28.2 }
+  ])
+  assert.deepEqual(normalizeDeviceGisPoints({ data: [{ equipmentId: 'EQ-2' }] }), [])
 })