This commit is contained in:
2026-10-30 11:04:23 +03:00
commit 2a19c16002
194 changed files with 24054 additions and 0 deletions

View File

@@ -0,0 +1,286 @@
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();