aspans_patrol/apps/api/src/modules/sites/sites.service.ts
2026-10-22 10:53:02 +05:00

94 lines
4.3 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 { BadRequestException, Injectable } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource, In } from 'typeorm';
import { Site } from '../../entities';
import { AccessService } from '../../common/access.service';
import { AuthUser, requireOrg } from '../../common/auth';
import { assertMultiPolygon } from '../../common/geo';
import { obj, reqStr, str } from '../../common/input';
import { isValidTimeZone } from '../../common/time';
@Injectable()
export class SitesService {
constructor(
@InjectDataSource() private readonly db: DataSource,
private readonly access: AccessService,
) {}
async list(user: AuthUser) {
const ids = await this.access.siteIds(user);
if (!ids.length) return [];
const sites = await this.db.getRepository(Site).find({ where: { id: In(ids) }, order: { name: 'ASC' } });
const counts: { site_id: string; routes: number; rounds_today: number }[] = await this.db.query(
`SELECT s.id AS site_id,
(SELECT count(*)::int FROM routes r WHERE r.site_id = s.id AND r.archived_at IS NULL) AS routes,
(SELECT count(*)::int FROM rounds o WHERE o.site_id = s.id
AND o.planned_start >= date_trunc('day', now() AT TIME ZONE s.timezone) AT TIME ZONE s.timezone
AND o.planned_start < (date_trunc('day', now() AT TIME ZONE s.timezone) + interval '1 day') AT TIME ZONE s.timezone
) AS rounds_today
FROM sites s WHERE s.id = ANY($1)`,
[ids],
);
const byId = new Map(counts.map((c) => [c.site_id, c]));
return sites.map((s) => ({ ...s, routesCount: byId.get(s.id)?.routes ?? 0, roundsToday: byId.get(s.id)?.rounds_today ?? 0 }));
}
get(user: AuthUser, id: string) {
return this.access.site(user, id);
}
// Проверяем геометрию средствами PostGIS и считаем центр для карты.
private async geometry(perimeter: unknown) {
if (perimeter === null) return { perimeter: null, center: null };
const mp = assertMultiPolygon(perimeter, 'perimeter');
const [row] = await this.db.query(
`SELECT ST_IsValid(g) AS valid, ST_IsValidReason(g) AS reason, ST_AsGeoJSON(ST_PointOnSurface(g))::json AS center
FROM (SELECT ST_SetSRID(ST_GeomFromGeoJSON($1), 4326) AS g) t`,
[JSON.stringify(mp)],
);
if (!row.valid) throw new BadRequestException(`Периметр нарисован с ошибкой (${row.reason}). Проверьте, что стороны не пересекаются`);
return { perimeter: mp, center: row.center };
}
private fields(o: Record<string, unknown>) {
const timezone = str(o, 'timezone', { max: 64 });
if (timezone && !isValidTimeZone(timezone)) throw new BadRequestException('Неизвестный часовой пояс');
return {
...(o.name !== undefined ? { name: reqStr(o, 'name', { max: 300 }) } : {}),
...('address' in o ? { address: str(o, 'address', { max: 500 }) ?? null } : {}),
...('description' in o ? { description: str(o, 'description', { max: 5000 }) ?? null } : {}),
...(timezone ? { timezone } : {}),
};
}
async create(user: AuthUser, body: unknown) {
const orgId = requireOrg(user);
const o = obj(body);
reqStr(o, 'name');
const geo = 'perimeter' in o ? await this.geometry(o.perimeter) : {};
const site = await this.db.getRepository(Site).save({
organizationId: orgId,
createdBy: user.id,
...this.fields(o),
...geo,
} as Partial<Site>);
return this.access.site(user, site.id).catch(() => site);
}
async update(user: AuthUser, id: string, body: unknown) {
await this.access.site(user, id);
const o = obj(body);
const geo = 'perimeter' in o ? await this.geometry(o.perimeter) : {};
await this.db.getRepository(Site).update({ id }, { ...this.fields(o), ...geo } as Partial<Site>);
return this.access.site(user, id);
}
async archive(user: AuthUser, id: string) {
await this.access.site(user, id);
await this.db.getRepository(Site).update({ id }, { archivedAt: new Date() });
// Будущие обходы архивного объекта не нужны.
await this.db.query(`UPDATE rounds SET status = 'cancelled', updated_at = now() WHERE site_id = $1 AND status = 'planned'`, [id]);
return { id };
}
}