233 lines
7.3 KiB
TypeScript
233 lines
7.3 KiB
TypeScript
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<typeof setInterval> | null = null;
|
|
private smsPollTimer: ReturnType<typeof setInterval> | 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();
|