Files
illusory-mapp/apps/main/src/lib/domains/mobile/mobile.vm.svelte.ts
2026-03-01 09:26:26 +02:00

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();