aspans_patrol/apps/api/src/entities/index.ts

340 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryColumn,
PrimaryGeneratedColumn,
Unique,
UpdateDateColumn,
} from 'typeorm';
import type {
GeoJsonLineString,
GeoJsonMultiPolygon,
GeoJsonPoint,
MediaRequirement,
RoundStatus,
} from '@patrol/shared';
// TypeORM сам переводит geometry в GeoJSON при чтении и обратно при записи.
@Entity('organizations')
export class Organization {
@PrimaryGeneratedColumn('uuid') id: string;
@Column({ type: 'char', length: 12, unique: true }) bin: string;
@Column('text') name: string;
@Column({ type: 'text', name: 'legal_name', nullable: true }) legalName: string | null;
@Column({ type: 'text', name: 'legal_address', nullable: true }) legalAddress: string | null;
@Column({ type: 'text', name: 'director_name', nullable: true }) directorName: string | null;
@Column({ type: 'text', nullable: true }) phone: string | null;
@Column({ type: 'text', nullable: true }) email: string | null;
@Column({ type: 'jsonb', name: 'registry_data', nullable: true }) registryData: unknown | null;
@Column({ type: 'timestamptz', name: 'registry_synced_at', nullable: true }) registrySyncedAt: Date | null;
@Column({ type: 'text', default: 'active' }) status: 'active' | 'blocked';
@CreateDateColumn({ type: 'timestamptz', name: 'created_at' }) createdAt: Date;
}
@Entity('roles')
export class Role {
@PrimaryGeneratedColumn('uuid') id: string;
@Column({ type: 'uuid', name: 'organization_id' }) organizationId: string;
@Column('text') code: string;
@Column('text') name: string;
@Column({ type: 'boolean', name: 'is_system', default: false }) isSystem: boolean;
@Column({ type: 'boolean', name: 'all_sites', default: false }) allSites: boolean;
@Column({ type: 'text', array: true, default: () => "'{}'" }) permissions: string[];
@CreateDateColumn({ type: 'timestamptz', name: 'created_at' }) createdAt: Date;
}
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid') id: string;
@Column({ type: 'uuid', name: 'organization_id', nullable: true }) organizationId: string | null;
// Человек из паспорта. Пароли и вход — только через паспорт / Aspans ID.
@Column({ type: 'uuid', name: 'passport_uuid', unique: true, nullable: true }) passportUuid: string | null;
// Логин паспорта (username) — для отображения.
@Column({ type: 'text', nullable: true }) login: string | null;
@Column({ type: 'text', name: 'last_name', nullable: true }) lastName: string | null;
@Column({ type: 'text', name: 'first_name', nullable: true }) firstName: string | null;
@Column({ type: 'text', name: 'middle_name', nullable: true }) middleName: string | null;
@Column({ type: 'text', name: 'full_name' }) fullName: string;
@Column({ type: 'text', nullable: true }) email: string | null;
@Column({ type: 'text', nullable: true }) phone: string | null;
@Column({ type: 'uuid', name: 'role_id', nullable: true }) roleId: string | null;
@ManyToOne(() => Role, { nullable: true })
@JoinColumn({ name: 'role_id' })
role: Role | null;
// Доступ в patrol (выключает администратор организации). Паспорт этим не управляет.
@Column({ type: 'boolean', name: 'is_active', default: true }) isActive: boolean;
// is_enabled из паспорта: отключили человека в паспорте — не пускаем и сюда.
@Column({ type: 'boolean', name: 'passport_enabled', default: true }) passportEnabled: boolean;
// Убрали с платформы patrol в паспорте (или удалили из организации здесь). Строку не удаляем:
// на человека ссылаются обходы, отметки и фото в отчётах.
@Column({ type: 'timestamptz', name: 'removed_at', nullable: true }) removedAt: Date | null;
@Column({ type: 'boolean', name: 'is_platform_admin', default: false }) isPlatformAdmin: boolean;
@Column({ type: 'timestamptz', name: 'last_login_at', nullable: true }) lastLoginAt: Date | null;
@CreateDateColumn({ type: 'timestamptz', name: 'created_at' }) createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz', name: 'updated_at' }) updatedAt: Date;
}
@Entity('user_sites')
export class UserSite {
@PrimaryColumn({ type: 'uuid', name: 'user_id' }) userId: string;
@PrimaryColumn({ type: 'uuid', name: 'site_id' }) siteId: string;
}
@Entity('user_sessions')
export class UserSession {
@PrimaryGeneratedColumn('uuid') id: string;
@Column({ type: 'uuid', name: 'user_id' }) userId: string;
@Column({ type: 'text', name: 'refresh_hash' }) refreshHash: string;
@Column({ type: 'text', name: 'user_agent', nullable: true }) userAgent: string | null;
@Column({ type: 'timestamptz', name: 'expires_at' }) expiresAt: Date;
@Column({ type: 'timestamptz', name: 'revoked_at', nullable: true }) revokedAt: Date | null;
@CreateDateColumn({ type: 'timestamptz', name: 'created_at' }) createdAt: Date;
}
@Entity('audit_log')
export class AuditLog {
@PrimaryGeneratedColumn('uuid') id: string;
@Column({ type: 'uuid', name: 'organization_id', nullable: true }) organizationId: string | null;
@Column({ type: 'uuid', name: 'user_id', nullable: true }) userId: string | null;
@Column('text') action: string;
@Column({ type: 'text', nullable: true }) entity: string | null;
@Column({ type: 'text', name: 'entity_id', nullable: true }) entityId: string | null;
@Column({ type: 'jsonb', nullable: true }) changes: unknown | null;
@CreateDateColumn({ type: 'timestamptz', name: 'created_at' }) createdAt: Date;
}
@Entity('sites')
export class Site {
@PrimaryGeneratedColumn('uuid') id: string;
@Column({ type: 'uuid', name: 'organization_id' }) organizationId: string;
@Column('text') name: string;
@Column({ type: 'text', nullable: true }) address: string | null;
@Column({ type: 'text', nullable: true }) description: string | null;
@Column({ type: 'geometry', spatialFeatureType: 'MultiPolygon', srid: 4326, nullable: true })
perimeter: GeoJsonMultiPolygon | null;
@Column({ type: 'geometry', spatialFeatureType: 'Point', srid: 4326, nullable: true })
center: GeoJsonPoint | null;
@Column({ type: 'text', default: 'Asia/Almaty' }) timezone: string;
@Column({ type: 'uuid', name: 'created_by', nullable: true }) createdBy: string | null;
@CreateDateColumn({ type: 'timestamptz', name: 'created_at' }) createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz', name: 'updated_at' }) updatedAt: Date;
@Column({ type: 'timestamptz', name: 'archived_at', nullable: true }) archivedAt: Date | null;
}
@Entity('routes')
export class Route {
@PrimaryGeneratedColumn('uuid') id: string;
@Column({ type: 'uuid', name: 'site_id' }) siteId: string;
@Column('text') name: string;
@Column({ type: 'text', nullable: true }) description: string | null;
@Column({ type: 'geometry', spatialFeatureType: 'LineString', srid: 4326, nullable: true })
path: GeoJsonLineString | null;
@Column({ type: 'int', name: 'expected_minutes', nullable: true }) expectedMinutes: number | null;
@Column({ type: 'boolean', name: 'strict_order', default: false }) strictOrder: boolean;
@Column({ type: 'int', default: 1 }) version: number;
@CreateDateColumn({ type: 'timestamptz', name: 'created_at' }) createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz', name: 'updated_at' }) updatedAt: Date;
@Column({ type: 'timestamptz', name: 'archived_at', nullable: true }) archivedAt: Date | null;
}
@Entity('checkpoints')
export class Checkpoint {
@PrimaryGeneratedColumn('uuid') id: string;
@Column({ type: 'uuid', name: 'route_id' }) routeId: string;
@Column('int') position: number;
@Column('text') name: string;
@Column({ type: 'geometry', spatialFeatureType: 'Point', srid: 4326 }) location: GeoJsonPoint;
@Column({ type: 'int', name: 'radius_m', default: 30 }) radiusM: number;
@Column({ type: 'text', name: 'media_required', default: 'photo' }) mediaRequired: MediaRequirement;
@Column({ type: 'text', nullable: true }) instruction: string | null;
@Column({ type: 'timestamptz', name: 'archived_at', nullable: true }) archivedAt: Date | null;
}
// Пункт чеклиста — общий для всех чеклистов компании.
@Entity('checklist_items')
export class ChecklistItem {
@PrimaryGeneratedColumn('uuid') id: string;
@Column({ type: 'uuid', name: 'organization_id' }) organizationId: string;
@Column('text') text: string;
@CreateDateColumn({ type: 'timestamptz', name: 'created_at' }) createdAt: Date;
}
@Entity('checklists')
export class Checklist {
@PrimaryGeneratedColumn('uuid') id: string;
@Column({ type: 'uuid', name: 'organization_id' }) organizationId: string;
@Column('text') name: string;
@Column({ type: 'text', nullable: true }) description: string | null;
@CreateDateColumn({ type: 'timestamptz', name: 'created_at' }) createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz', name: 'updated_at' }) updatedAt: Date;
@Column({ type: 'timestamptz', name: 'archived_at', nullable: true }) archivedAt: Date | null;
}
@Entity('checklist_entries')
export class ChecklistEntry {
@PrimaryColumn({ type: 'uuid', name: 'checklist_id' }) checklistId: string;
@PrimaryColumn({ type: 'uuid', name: 'item_id' }) itemId: string;
@Column('int') position: number;
}
@Entity('checkpoint_checklists')
export class CheckpointChecklist {
@PrimaryColumn({ type: 'uuid', name: 'checkpoint_id' }) checkpointId: string;
@PrimaryColumn({ type: 'uuid', name: 'checklist_id' }) checklistId: string;
@Column('int') position: number;
}
@Entity('patrol_schedules')
export class PatrolSchedule {
@PrimaryGeneratedColumn('uuid') id: string;
@Column({ type: 'uuid', name: 'site_id' }) siteId: string;
@Column({ type: 'uuid', name: 'route_id' }) routeId: string;
@Column('text') name: string;
// Время старта 'HH:MM' в часовом поясе объекта
@Column({ type: 'text', name: 'start_times', array: true }) startTimes: string[];
@Column({ type: 'int', name: 'window_minutes', default: 60 }) windowMinutes: number;
@Column({ type: 'int', name: 'late_tolerance_minutes', default: 15 }) lateToleranceMinutes: number;
@Column({ type: 'date', name: 'valid_from' }) validFrom: string;
@Column({ type: 'date', name: 'valid_to', nullable: true }) validTo: string | null;
@Column({ type: 'boolean', name: 'is_active', default: true }) isActive: boolean;
@CreateDateColumn({ type: 'timestamptz', name: 'created_at' }) createdAt: Date;
}
@Entity('schedule_assignments')
@Unique(['scheduleId', 'userId', 'date'])
export class ScheduleAssignment {
@PrimaryGeneratedColumn('uuid') id: string;
@Column({ type: 'uuid', name: 'schedule_id' }) scheduleId: string;
@Column({ type: 'uuid', name: 'user_id' }) userId: string;
@Column({ type: 'date' }) date: string;
@Column({ type: 'uuid', name: 'created_by', nullable: true }) createdBy: string | null;
@CreateDateColumn({ type: 'timestamptz', name: 'created_at' }) createdAt: Date;
}
export type RouteSnapshot = {
version: number;
name: string;
strictOrder: boolean;
path: GeoJsonLineString | null;
checkpoints: {
id: string;
position: number;
name: string;
location: GeoJsonPoint;
radiusM: number;
mediaRequired: MediaRequirement;
instruction: string | null;
// В снимках до появления чеклистов поля нет — читать как [].
checklists?: SnapshotChecklist[];
}[];
};
export type SnapshotChecklist = { id: string; name: string; items: { id: string; text: string }[] };
// Ответ охранника по пункту. Текст копируем: отчёт читается и после правки пункта.
export type ChecklistAnswer = { checklistId: string; itemId: string; text: string; checked: boolean };
@Entity('rounds')
@Index(['siteId', 'plannedStart'])
export class Round {
@PrimaryGeneratedColumn('uuid') id: string;
@Column({ type: 'uuid', name: 'site_id' }) siteId: string;
@Column({ type: 'uuid', name: 'route_id' }) routeId: string;
@Column({ type: 'uuid', name: 'schedule_id', nullable: true }) scheduleId: string | null;
// Дата графика (в поясе объекта), по ней ищем дежурных
@Column({ type: 'date', name: 'duty_date', nullable: true }) dutyDate: string | null;
@Column({ type: 'uuid', name: 'performed_by', nullable: true }) performedBy: string | null;
@Column({ type: 'uuid', name: 'created_by', nullable: true }) createdBy: string | null;
@Column({ type: 'timestamptz', name: 'planned_start' }) plannedStart: Date;
@Column({ type: 'timestamptz', name: 'planned_end' }) plannedEnd: Date;
@Column({ type: 'int', name: 'late_tolerance_minutes', default: 15 }) lateToleranceMinutes: number;
@Column({ type: 'timestamptz', name: 'started_at', nullable: true }) startedAt: Date | null;
@Column({ type: 'timestamptz', name: 'finished_at', nullable: true }) finishedAt: Date | null;
@Column({ type: 'text', default: 'planned' }) status: RoundStatus;
@Column({ type: 'jsonb', name: 'route_snapshot' }) routeSnapshot: RouteSnapshot;
@Column({ type: 'int', name: 'checkpoints_total', default: 0 }) checkpointsTotal: number;
@Column({ type: 'int', name: 'checkpoints_done', default: 0 }) checkpointsDone: number;
@Column({ type: 'text', nullable: true }) comment: string | null;
@CreateDateColumn({ type: 'timestamptz', name: 'created_at' }) createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz', name: 'updated_at' }) updatedAt: Date;
}
@Entity('checkpoint_visits')
export class CheckpointVisit {
// id генерирует телефон: повтор из офлайн-очереди не создаёт дубль
@PrimaryColumn('uuid') id: string;
@Column({ type: 'uuid', name: 'round_id' }) roundId: string;
@Column({ type: 'uuid', name: 'checkpoint_id', nullable: true }) checkpointId: string | null;
@Column({ type: 'uuid', name: 'user_id' }) userId: string;
@Column({ type: 'geometry', name: 'marked_location', spatialFeatureType: 'Point', srid: 4326 })
markedLocation: GeoJsonPoint;
@Column({ type: 'geometry', name: 'device_location', spatialFeatureType: 'Point', srid: 4326, nullable: true })
deviceLocation: GeoJsonPoint | null;
@Column({ type: 'real', name: 'device_accuracy_m', nullable: true }) deviceAccuracyM: number | null;
@Column({ type: 'real', name: 'distance_to_checkpoint_m', nullable: true }) distanceToCheckpointM: number | null;
@Column({ type: 'real', name: 'gps_distance_m', nullable: true }) gpsDistanceM: number | null;
@Column({ type: 'text', array: true, default: () => "'{}'" }) flags: string[];
@Column({ type: 'text', nullable: true }) note: string | null;
@Column({ type: 'boolean', name: 'is_issue', default: false }) isIssue: boolean;
@Column({ type: 'jsonb', default: () => "'[]'" }) checklist: ChecklistAnswer[];
@Column({ type: 'timestamptz', name: 'client_created_at' }) clientCreatedAt: Date;
@CreateDateColumn({ type: 'timestamptz', name: 'created_at' }) createdAt: Date;
}
@Entity('media')
export class Media {
// id тоже генерирует телефон
@PrimaryColumn('uuid') id: string;
@Column({ type: 'uuid', name: 'organization_id' }) organizationId: string;
@Column({ type: 'uuid', name: 'uploaded_by' }) uploadedBy: string;
@Column({ type: 'uuid', name: 'visit_id', nullable: true }) visitId: string | null;
@Column('text') kind: 'photo' | 'video';
@Column('text') mime: string;
@Column({ type: 'bigint', name: 'size_bytes' }) sizeBytes: string;
@Column({ type: 'text', nullable: true }) filename: string | null;
@Column({ type: 'text', name: 'storage_key' }) storageKey: string;
@Column({ type: 'text', name: 'upload_id', nullable: true }) uploadId: string | null;
@Column({ type: 'jsonb', default: () => "'[]'" }) parts: { partNumber: number; etag: string; size: number }[];
@Column({ type: 'text', default: 'uploading' }) status: 'uploading' | 'ready';
@Column({ type: 'timestamptz', name: 'captured_at', nullable: true }) capturedAt: Date | null;
@CreateDateColumn({ type: 'timestamptz', name: 'created_at' }) createdAt: Date;
}
@Entity('webhooks')
export class Webhook {
@PrimaryGeneratedColumn('uuid') id: string;
@Column({ type: 'uuid', name: 'organization_id' }) organizationId: string;
@Column('text') url: string;
@Column({ type: 'text', array: true }) events: string[];
@Column('text') secret: string;
@Column({ type: 'boolean', name: 'is_active', default: true }) isActive: boolean;
@CreateDateColumn({ type: 'timestamptz', name: 'created_at' }) createdAt: Date;
}
export const ENTITIES = [
Organization,
Role,
User,
UserSite,
UserSession,
AuditLog,
Site,
Route,
Checkpoint,
ChecklistItem,
Checklist,
ChecklistEntry,
CheckpointChecklist,
PatrolSchedule,
ScheduleAssignment,
Round,
CheckpointVisit,
Media,
Webhook,
];