287 lines
7.7 KiB
TypeScript
287 lines
7.7 KiB
TypeScript
import { makeAutoObservable, reaction, runInAction } from 'mobx';
|
|
import { AppState, type AppStateStatus } from 'react-native';
|
|
import { themeStore } from '@/shared/config';
|
|
import { appEvents, currentPosition, hydrateStorage, startGeoTracking, stopGeoTracking, storage } from '@/shared/lib';
|
|
import type { Driver, MrmAdmin } from '@/entities/driver/@x/session';
|
|
import { MessageStore } from '@/entities/message/@x/session';
|
|
import { loginByPin, loginMrm as loginMrmRequest, logout as logoutRequest, sendHeartbeat } from '@/entities/driver/@x/session';
|
|
import type { AppScreen, ThemeMode } from './types';
|
|
import {
|
|
clearMrmAdmin,
|
|
clearSession,
|
|
persistDriver,
|
|
persistMrmAdmin,
|
|
persistSession,
|
|
sessionStartedAt,
|
|
storedDriver,
|
|
storedMrmAdmin,
|
|
storedToken,
|
|
} from './sessionStorage';
|
|
|
|
const HEARTBEAT_INTERVAL = 10_000;
|
|
const SPEED_INTERVAL = 3_000;
|
|
|
|
function themeStorageKey(driverId: string): string {
|
|
return `kiosk:theme:${driverId}`;
|
|
}
|
|
|
|
export class SessionStore {
|
|
readonly messageStore = new MessageStore();
|
|
|
|
screen: AppScreen = 'login';
|
|
driver: Driver | null = null;
|
|
mrmAdmin: MrmAdmin | null = null;
|
|
sessionKicked = false;
|
|
ready = false;
|
|
|
|
theme: ThemeMode = 'dark';
|
|
isDark = true;
|
|
|
|
isMoving = true;
|
|
speed = 32;
|
|
currentStopIndex = 3;
|
|
|
|
loginError: string | null = null;
|
|
loginLoading = false;
|
|
|
|
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
private speedTimer: ReturnType<typeof setInterval> | null = null;
|
|
private appStateSub: { remove: () => void } | null = null;
|
|
|
|
constructor() {
|
|
makeAutoObservable(this, {}, { autoBind: true });
|
|
reaction(
|
|
() => this.isDark,
|
|
(isDark) => themeStore.setDark(isDark),
|
|
{ fireImmediately: true },
|
|
);
|
|
}
|
|
|
|
async init(): Promise<void> {
|
|
if (this.ready) return;
|
|
await hydrateStorage();
|
|
runInAction(() => {
|
|
this.driver = storedDriver();
|
|
this.mrmAdmin = storedMrmAdmin();
|
|
this.restore();
|
|
this.ready = true;
|
|
});
|
|
this.start();
|
|
}
|
|
|
|
get messages() {
|
|
return this.messageStore.messages;
|
|
}
|
|
|
|
get connection() {
|
|
return this.messageStore.connection;
|
|
}
|
|
|
|
get pendingImportant() {
|
|
return this.messageStore.pendingImportant;
|
|
}
|
|
|
|
get pendingCount() {
|
|
return this.messageStore.pendingCount;
|
|
}
|
|
|
|
get unreadCount() {
|
|
return this.messageStore.unreadCount;
|
|
}
|
|
|
|
private restore(): void {
|
|
if (this.mrmAdmin) {
|
|
this.screen = 'main';
|
|
} else if (storedToken() && this.driver) {
|
|
this.screen = 'main';
|
|
this.messageStore.hydrateFromCache();
|
|
}
|
|
if (this.driver) this.applyStoredTheme(this.driver.id);
|
|
this.isDark = this.theme === 'dark';
|
|
}
|
|
|
|
start(): void {
|
|
this.speedTimer = setInterval(() => {
|
|
if (this.screen !== 'main') return;
|
|
runInAction(() => {
|
|
this.speed = Math.max(0, Math.min(60, this.speed + (Math.random() - 0.5) * 8));
|
|
});
|
|
}, SPEED_INTERVAL);
|
|
this.startSessionLoop();
|
|
}
|
|
|
|
stop(): void {
|
|
if (this.speedTimer) clearInterval(this.speedTimer);
|
|
this.stopSessionLoop();
|
|
this.messageStore.stop();
|
|
}
|
|
|
|
private startSessionLoop(): void {
|
|
if (this.screen === 'login' || this.screen === 'welcome') return;
|
|
startGeoTracking();
|
|
this.messageStore.start();
|
|
void this.heartbeat();
|
|
this.heartbeatTimer = setInterval(() => void this.heartbeat(), HEARTBEAT_INTERVAL);
|
|
this.appStateSub = AppState.addEventListener('change', (state: AppStateStatus) => {
|
|
if (state === 'active') void this.heartbeat();
|
|
});
|
|
}
|
|
|
|
private stopSessionLoop(): void {
|
|
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
this.heartbeatTimer = null;
|
|
this.appStateSub?.remove();
|
|
this.appStateSub = null;
|
|
stopGeoTracking();
|
|
}
|
|
|
|
private async heartbeat(): Promise<void> {
|
|
const result = await sendHeartbeat(
|
|
storedToken(),
|
|
currentPosition(),
|
|
this.driver?.deviationSettings?.updatedAt ?? null,
|
|
);
|
|
if (result.sessionExpired) {
|
|
runInAction(() => {
|
|
this.sessionKicked = true;
|
|
this.driver = null;
|
|
this.screen = 'login';
|
|
});
|
|
clearSession();
|
|
this.stopSessionLoop();
|
|
return;
|
|
}
|
|
if (result.deviationSettings && this.driver) {
|
|
runInAction(() => {
|
|
this.driver = { ...this.driver!, deviationSettings: result.deviationSettings! };
|
|
});
|
|
persistDriver(this.driver);
|
|
}
|
|
}
|
|
|
|
private applyStoredTheme(driverId: string): void {
|
|
const raw = storage.get(themeStorageKey(driverId));
|
|
if (raw === 'light' || raw === 'dark') this.theme = raw;
|
|
}
|
|
|
|
private mapDriver(raw: Record<string, unknown>): Driver {
|
|
return {
|
|
id: String(raw.id),
|
|
name: raw.name as string,
|
|
vehicleType: raw.vehicleType as Driver['vehicleType'],
|
|
vehicleNumber: raw.vehicleNumber as string,
|
|
routeNumber: raw.routeNumber as string,
|
|
routeId: (raw.routeId as string) ?? undefined,
|
|
shiftStart: (raw.shiftStart as string) || '08:00',
|
|
dispatcherName: (raw.dispatcherName as string) ?? undefined,
|
|
deviationSettings: (raw.deviationSettings as Driver['deviationSettings']) ?? undefined,
|
|
};
|
|
}
|
|
|
|
async login(employeeId: string, pin: string): Promise<boolean> {
|
|
this.loginError = null;
|
|
this.loginLoading = true;
|
|
this.sessionKicked = false;
|
|
try {
|
|
const { driver, token } = await loginByPin(employeeId, pin);
|
|
persistSession(token, driver);
|
|
runInAction(() => {
|
|
this.driver = driver;
|
|
this.messageStore.reset();
|
|
});
|
|
this.startShift();
|
|
return true;
|
|
} catch (error) {
|
|
runInAction(() => {
|
|
this.loginError = error instanceof Error ? error.message : 'Ошибка входа';
|
|
});
|
|
return false;
|
|
} finally {
|
|
runInAction(() => (this.loginLoading = false));
|
|
}
|
|
}
|
|
|
|
loginSmsVerified(data: { token: string; driver: Record<string, unknown> }): void {
|
|
const driver = this.mapDriver(data.driver);
|
|
persistSession(data.token, driver);
|
|
this.driver = driver;
|
|
this.messageStore.reset();
|
|
this.loginError = null;
|
|
this.startShift();
|
|
}
|
|
|
|
async loginMrm(login: string, password: string): Promise<void> {
|
|
this.loginError = null;
|
|
this.loginLoading = true;
|
|
try {
|
|
const admin = await loginMrmRequest(login, password);
|
|
persistMrmAdmin(admin);
|
|
runInAction(() => {
|
|
this.mrmAdmin = admin;
|
|
this.screen = 'main';
|
|
});
|
|
this.startSessionLoop();
|
|
} catch (error) {
|
|
runInAction(() => {
|
|
this.loginError = error instanceof Error ? error.message : 'Ошибка входа';
|
|
});
|
|
} finally {
|
|
runInAction(() => (this.loginLoading = false));
|
|
}
|
|
}
|
|
|
|
startShift(): void {
|
|
this.screen = 'main';
|
|
this.isMoving = true;
|
|
this.startSessionLoop();
|
|
}
|
|
|
|
setScreen(screen: AppScreen): void {
|
|
this.screen = screen;
|
|
}
|
|
|
|
async logout(): Promise<void> {
|
|
const token = storedToken();
|
|
clearSession();
|
|
clearMrmAdmin();
|
|
this.stopSessionLoop();
|
|
this.messageStore.stop();
|
|
runInAction(() => {
|
|
this.driver = null;
|
|
this.mrmAdmin = null;
|
|
this.messageStore.reset();
|
|
this.screen = 'login';
|
|
this.isMoving = false;
|
|
});
|
|
await logoutRequest(token);
|
|
}
|
|
|
|
dismissSessionKicked(): void {
|
|
this.sessionKicked = false;
|
|
}
|
|
|
|
toggleTheme(): void {
|
|
this.theme = this.theme === 'light' ? 'dark' : 'light';
|
|
this.isDark = this.theme === 'dark';
|
|
this.persistTheme();
|
|
appEvents.emit('theme:changed', this.theme);
|
|
}
|
|
|
|
setTheme(theme: ThemeMode): void {
|
|
this.theme = theme;
|
|
this.isDark = this.theme === 'dark';
|
|
this.persistTheme();
|
|
}
|
|
|
|
private persistTheme(): void {
|
|
if (!this.driver) return;
|
|
storage.set(themeStorageKey(this.driver.id), this.theme);
|
|
}
|
|
|
|
get sessionStartedAt(): number | null {
|
|
return sessionStartedAt();
|
|
}
|
|
}
|
|
|
|
export const sessionStore = new SessionStore();
|