94 lines
4.3 KiB
TypeScript
94 lines
4.3 KiB
TypeScript
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 };
|
||
}
|
||
}
|