import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState, } from 'react' import { useNavigate, useOutletContext, useParams, useSearchParams, } from 'react-router-dom' import { AnimatePresence, m } from 'framer-motion' import { useQuery } from '@apollo/client/react' import type { Device as PageDevice } from './types' import { GET_DEVICE_PAGE_QUERY } from '../../entities/device/api/device.graphql' import type { Device as ApiDevice, DeviceNetworkStatus, DeviceTechnicalStatus, GetPhoneGpsTrackData, GetTelemetryData, GetDevicePageData, GetDevicePageVariables, } from '../../entities/device/model/types' import { DeviceMainCard } from './components/DeviceMainCard/DeviceMainCard' import { DeviceMapCard } from './components/DeviceMapCard/DeviceMapCard' import { DeviceActionsCard } from './components/DeviceActionsCard/DeviceActionsCard' import { DevicePermissionsCard } from './components/DevicePermissionsCard/DevicePermissionsCard' import { DeviceStatsCards } from './components/DeviceStatsCards/DeviceStatsCards' import { DeviceAppsView } from './components/DeviceAppsView/DeviceAppsView' import { pageSwapVariants, uiFadeTransition } from '../../shared/lib/motion' import { isPhonePolicyOptionEnabled } from '../../entities/device/lib/phonePolicy' import { EmptyState } from '../../shared/ui/EmptyState/EmptyState' import './DevicePage.scss' import { DevicePageSkeleton } from './components/DevicePageSkeleton/DevicePageSkeleton' import type { AppLayoutOutletContext } from '../../app/layouts/AppLayout' import { preloadOnIdle, useDeferredModalPayload, useLazyMount, } from '../../shared/lib/lazyMount' import { isPresent } from '../../shared/lib/isPresent' const loadDeviceHistoryModal = () => import('./components/DeviceHistoryModal/DeviceHistoryModal').then( (module) => ({ default: module.DeviceHistoryModal, }), ) const DeviceHistoryModal = lazy(loadDeviceHistoryModal) const loadDeviceMaintenanceModal = () => import('./components/DeviceMaintenanceModal/DeviceMaintenanceModal').then( (module) => ({ default: module.DeviceMaintenanceModal, }), ) const DeviceMaintenanceModal = lazy(loadDeviceMaintenanceModal) const loadDeviceMaintenanceHistoryModal = () => import( './components/DeviceMaintenanceHistoryModal/DeviceMaintenanceHistoryModal' ).then((module) => ({ default: module.DeviceMaintenanceHistoryModal, })) const DeviceMaintenanceHistoryModal = lazy( loadDeviceMaintenanceHistoryModal, ) function formatLocationDate(timestamp: number) { if (!timestamp) return 'Нет данных' return new Intl.DateTimeFormat('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }).format(new Date(timestamp)) } function formatTechValue(value?: number | null) { return typeof value === 'number' ? String(value) : undefined } function getLatestTelemetryItem(telemetry: GetTelemetryData['getTelemetry']) { if (!telemetry.length) return null return [...telemetry].sort((a, b) => b.date - a.date)[0] } function getSortedGpsTrack(track: GetPhoneGpsTrackData['getPhoneGpsTrack']) { return track.filter(isPresent).sort((a, b) => a.date - b.date) } function getDeviceNetworkConnection(status?: DeviceNetworkStatus) { if (status === 'Online') return 'online' if (status === 'Lost') return 'offlineDanger' return 'offline' } function getDeviceNetworkLabel(status?: DeviceNetworkStatus) { if (status === 'Online') return 'В сети' if (status === 'Lost') return 'Потерян' return 'Не в сети' } function getDeviceCondition(status?: DeviceTechnicalStatus) { if (status === 'InService') return 'service' if (status === 'NeedsMaintenance') return 'inspection' return 'ok' } function mapApiDeviceToPageDevice( device: ApiDevice, batteryLevel?: number, gpsTrack: GetPhoneGpsTrackData['getPhoneGpsTrack'] = [], telemetry: GetTelemetryData['getTelemetry'] = [], ): PageDevice { const sortedGpsTrack = getSortedGpsTrack(gpsTrack) const policy = device.policy const techState = device.techState const bluetoothEnabled = isPhonePolicyOptionEnabled( policy?.bluetooth ?? policy?.canUseBluetooth, ) const gpsEnabled = isPhonePolicyOptionEnabled(policy?.GPS ?? policy?.canUseGPS) const cameraEnabled = isPhonePolicyOptionEnabled( policy?.camera ?? policy?.canUseCamera, ) const simEnabled = isPhonePolicyOptionEnabled(policy?.sim ?? policy?.canUseSim) const isLocked = policy?.locked ?? false const currentGpsPoint = sortedGpsTrack.length > 0 ? sortedGpsTrack[sortedGpsTrack.length - 1] : null const routeGpsPoints = sortedGpsTrack.length > 1 ? sortedGpsTrack.slice(0, -1) : [] const location = currentGpsPoint ? { lat: currentGpsPoint.lat, lng: currentGpsPoint.lng, } : device.lastLocation ? { lat: device.lastLocation.lat, lng: device.lastLocation.lng, } : undefined const lastLocationAt = currentGpsPoint ? formatLocationDate(currentGpsPoint.date) : device.lastLocation ? formatLocationDate(device.lastLocation.date) : 'Нет данных' return { id: device.id, model: 'АРМАФОН S3.3+', factoryNumber: device.serial || 'Заводской номер не указан', imei: device.imei || 'IMEI не указан', imei2: device.imei2 || 'IMEI 2 не указан', serialNumber: device.serial || undefined, employee: device.org?.name ?? null, organisation: device.org?.name, organisationId: device.org?.id ?? device.orgId, policy, organisationPolicy: device.org?.policy ?? null, condition: getDeviceCondition(techState?.status), technicalStatus: techState?.status ?? 'Healthy', malfunctions: techState?.malfunctions ?? [], malfunctionComment: techState?.malfunctionComment, connection: getDeviceNetworkConnection(device.networkStatus), connectionText: getDeviceNetworkLabel(device.networkStatus), workTime: null, registeredAt: device.registerDate ? formatLocationDate(device.registerDate) : undefined, image: undefined, location, lastLocationAt, lastLocationDate: device.lastLocation?.date, route: routeGpsPoints.map((point) => ({ lat: point.lat, lng: point.lng, time: formatLocationDate(point.date), })), permissions: { wifi: false, bluetooth: bluetoothEnabled, gps: gpsEnabled, camera: cameraEnabled, sim: simEnabled, speaker: false, locked: isLocked, }, statusIcons: { gps: gpsEnabled, wifi: false, bluetooth: bluetoothEnabled, lock: isLocked, camera: cameraEnabled, sim: simEnabled, sound: false, kiosk: false, }, battery: techState?.batteryLevel ?? batteryLevel, batteryMaxCapacity: techState?.batteryRemainingCapacity ?? undefined, telemetry, chargeCycles: formatTechValue(techState?.batteryCycles), totalWorkTime: techState?.worktime ?? null, mediumImpacts: formatTechValue(techState?.hits), overheats: formatTechValue(techState?.overheats), } } export function DevicePage() { const { deviceId } = useParams() const navigate = useNavigate() const { setNavbarBreadcrumbs } = useOutletContext() const [searchParams, setSearchParams] = useSearchParams() const [isHistoryOpen, setIsHistoryOpen] = useState(false) const shouldMountHistoryModal = useLazyMount(isHistoryOpen) const [maintenanceMode, setMaintenanceMode] = useState< 'view' | 'manage' | null >(null) const renderMaintenanceMode = useDeferredModalPayload(maintenanceMode) const shouldMountMaintenanceModal = useLazyMount(maintenanceMode !== null) const [isMaintenanceHistoryOpen, setIsMaintenanceHistoryOpen] = useState(false) const shouldMountMaintenanceHistoryModal = useLazyMount( isMaintenanceHistoryOpen, ) const maintenanceHistoryTimerRef = useRef(null) const numericDeviceId = Number(deviceId) const isAppsView = searchParams.get('view') === 'apps' useEffect(() => { return preloadOnIdle([ loadDeviceHistoryModal, loadDeviceMaintenanceModal, loadDeviceMaintenanceHistoryModal, ]) }, []) useEffect(() => { return () => { if (maintenanceHistoryTimerRef.current !== null) { window.clearTimeout(maintenanceHistoryTimerRef.current) } } }, []) const { data, loading, error, refetch, startPolling, stopPolling, } = useQuery< GetDevicePageData, GetDevicePageVariables >(GET_DEVICE_PAGE_QUERY, { variables: { id: numericDeviceId, phoneId: String(numericDeviceId), telemetryStartDate: 0, gpsStartDate: 0, }, skip: !numericDeviceId, fetchPolicy: 'network-only', pollInterval: 15000, }) const handlePermissionsEditingChange = useCallback( (isEditing: boolean) => { if (isEditing) { stopPolling() return } if (numericDeviceId) { startPolling(15000) } }, [numericDeviceId, startPolling, stopPolling], ) const latestTelemetryItem = useMemo(() => { return getLatestTelemetryItem(data?.getTelemetry ?? []) }, [data]) const device = useMemo(() => { if (!data?.getPhone) return null return mapApiDeviceToPageDevice( data.getPhone, latestTelemetryItem?.batteryLevel ?? undefined, data.getPhoneGpsTrack ?? [], data.getTelemetry ?? [], ) }, [data, latestTelemetryItem]) useEffect(() => { if (!device) { setNavbarBreadcrumbs([]) return } setNavbarBreadcrumbs([ { label: device.factoryNumber, to: isAppsView ? `/devices/${device.id}` : undefined, }, ...(isAppsView ? [{ label: 'Приложения' }] : []), ]) return () => { setNavbarBreadcrumbs([]) } }, [device, isAppsView, setNavbarBreadcrumbs]) const handleOpenApps = useCallback(() => { const nextParams = new URLSearchParams(searchParams) nextParams.set('view', 'apps') setSearchParams(nextParams) }, [searchParams, setSearchParams]) const handleCloseApps = useCallback(() => { const nextParams = new URLSearchParams(searchParams) nextParams.delete('view') setSearchParams(nextParams, { replace: true, }) }, [searchParams, setSearchParams]) const handleOpenHistory = useCallback(() => { setIsHistoryOpen(true) }, []) const handleOpenMaintenanceDetails = useCallback(() => { setMaintenanceMode('view') }, []) const handleManageMaintenance = useCallback(() => { setMaintenanceMode('manage') }, []) const handleOpenMaintenanceHistory = useCallback(() => { setMaintenanceMode(null) if (maintenanceHistoryTimerRef.current !== null) { window.clearTimeout(maintenanceHistoryTimerRef.current) } maintenanceHistoryTimerRef.current = window.setTimeout(() => { setIsMaintenanceHistoryOpen(true) maintenanceHistoryTimerRef.current = null }, 220) }, []) const handleOpenMaintenanceHistoryFromCard = useCallback(() => { setIsMaintenanceHistoryOpen(true) }, []) const handleMaintenanceUpdated = useCallback(async () => { await refetch() }, [refetch]) const handleRefetchPolicy = useCallback(async () => { const result = await refetch() return result.data?.getPhone?.policy }, [refetch]) const handleLockStateChange = useCallback(async () => { const result = await refetch() return result.data?.getPhone?.policy?.locked }, [refetch]) if (!numericDeviceId) { return (
) } if (loading && !device) { return } if (error || !device) { return (
) } return (
{isAppsView ? ( ) : (
)}
{shouldMountHistoryModal && ( )} {shouldMountMaintenanceModal && renderMaintenanceMode && ( { if (!nextOpen) setMaintenanceMode(null) }} onUpdated={handleMaintenanceUpdated} /> )} {shouldMountMaintenanceHistoryModal && ( )}
) }