import type { MobileDevice, MobileDeviceDetail, MobileMediaAsset, MobileSMS, } from "@pkg/logic/domains/mobile/data"; import { getDeviceDetailSQ, getDeviceMediaSQ, getDeviceSmsSQ, getDevicesSQ, } from "./mobile.remote"; import { toast } from "svelte-sonner"; class MobileViewModel { devices = $state([] as MobileDevice[]); devicesLoading = $state(false); devicesSearch = $state(""); devicesPage = $state(1); devicesPageSize = $state(25); devicesTotal = $state(0); selectedDeviceDetail = $state(null as MobileDeviceDetail | null); deviceDetailLoading = $state(false); sms = $state([] as MobileSMS[]); smsLoading = $state(false); smsPage = $state(1); smsPageSize = $state(25); smsTotal = $state(0); media = $state([] as MobileMediaAsset[]); mediaLoading = $state(false); mediaPage = $state(1); mediaPageSize = $state(25); mediaTotal = $state(0); private devicesPollTimer: ReturnType | null = null; private smsPollTimer: ReturnType | null = null; private devicesRequestVersion = 0; async fetchDevices({ showLoading = true, forceRefresh = false, }: { showLoading?: boolean; forceRefresh?: boolean; } = {}) { const requestVersion = ++this.devicesRequestVersion; if (showLoading) { this.devicesLoading = true; } try { const result = await getDevicesSQ({ search: this.devicesSearch || undefined, refreshAt: forceRefresh ? Date.now() : undefined, pagination: { page: this.devicesPage, pageSize: this.devicesPageSize, sortBy: "lastPingAt", sortOrder: "desc", }, }); if (requestVersion !== this.devicesRequestVersion) { return; } if (result?.error || !result?.data) { toast.error(result?.error?.message || "Failed to load devices", { description: result?.error?.description || "Please try again later", }); return; } this.devices = [...(result.data.data as MobileDevice[])]; this.devicesTotal = result.data.total; } catch (error) { if (requestVersion !== this.devicesRequestVersion) { return; } toast.error("Failed to load devices", { description: error instanceof Error ? error.message : "Please try again later", }); } finally { if (showLoading && requestVersion === this.devicesRequestVersion) { this.devicesLoading = false; } } } async refreshDevices() { await this.fetchDevices({ showLoading: true, forceRefresh: true }); } async fetchDeviceDetail(deviceId: number) { this.deviceDetailLoading = true; try { const result = await getDeviceDetailSQ({ deviceId }); if (result?.error || !result?.data) { toast.error(result?.error?.message || "Failed to load device details", { description: result?.error?.description || "Please try again later", }); return; } this.selectedDeviceDetail = result.data as MobileDeviceDetail; } catch (error) { toast.error("Failed to load device details", { description: error instanceof Error ? error.message : "Please try again later", }); } finally { this.deviceDetailLoading = false; } } async fetchSMS(deviceId: number) { this.smsLoading = true; try { const result = await getDeviceSmsSQ({ deviceId, pagination: { page: this.smsPage, pageSize: this.smsPageSize, sortBy: "sentAt", sortOrder: "desc", }, }); if (result?.error || !result?.data) { toast.error(result?.error?.message || "Failed to load SMS", { description: result?.error?.description || "Please try again later", }); return; } this.sms = result.data.data as MobileSMS[]; this.smsTotal = result.data.total; } catch (error) { toast.error("Failed to load SMS", { description: error instanceof Error ? error.message : "Please try again later", }); } finally { this.smsLoading = false; } } async fetchMedia(deviceId: number) { this.mediaLoading = true; try { const result = await getDeviceMediaSQ({ deviceId, pagination: { page: this.mediaPage, pageSize: this.mediaPageSize, sortBy: "createdAt", sortOrder: "desc", }, }); if (result?.error || !result?.data) { toast.error(result?.error?.message || "Failed to load media assets", { description: result?.error?.description || "Please try again later", }); return; } this.media = result.data.data as MobileMediaAsset[]; this.mediaTotal = result.data.total; } catch (error) { toast.error("Failed to load media assets", { description: error instanceof Error ? error.message : "Please try again later", }); } finally { this.mediaLoading = false; } } startDevicesPolling(intervalMs = 5000) { this.stopDevicesPolling(); this.devicesPollTimer = setInterval(() => { void this.fetchDevices({ showLoading: false, forceRefresh: true, }); }, intervalMs); } stopDevicesPolling() { if (this.devicesPollTimer) { clearInterval(this.devicesPollTimer); this.devicesPollTimer = null; } } startSmsPolling(deviceId: number, intervalMs = 5000) { this.stopSmsPolling(); this.smsPollTimer = setInterval(() => { this.fetchSMS(deviceId); }, intervalMs); } stopSmsPolling() { if (this.smsPollTimer) { clearInterval(this.smsPollTimer); this.smsPollTimer = null; } } formatLastPing(lastPingAt: Date | null | undefined) { if (!lastPingAt) { return "Never"; } const pingDate = lastPingAt instanceof Date ? lastPingAt : new Date(lastPingAt); const diffSeconds = Math.floor((Date.now() - pingDate.getTime()) / 1000); if (diffSeconds < 60) { return `${diffSeconds}s ago`; } if (diffSeconds < 3600) { return `${Math.floor(diffSeconds / 60)}m ago`; } if (diffSeconds < 86400) { return `${Math.floor(diffSeconds / 3600)}h ago`; } return pingDate.toLocaleString(); } } export const mobileVM = new MobileViewModel();