init
This commit is contained in:
286
src/entities/session/model/sessionStore.ts
Normal file
286
src/entities/session/model/sessionStore.ts
Normal 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();
|
||||
Reference in New Issue
Block a user