Исправлено дублирование сообщений в task-чате: защита от двойного Enter/клика по кнопке отправки

This commit is contained in:
2026-08-24 16:53:25 +03:00
parent 8b713e66c0
commit c0e2ddd9c8
11 changed files with 1 additions and 1666 deletions

View File

@@ -1,5 +1,5 @@
import { sql, relations } from "drizzle-orm";
import { pgTable, text, varchar, integer, boolean, timestamp, jsonb, serial, bigserial, doublePrecision, unique, foreignKey, uniqueIndex, index, numeric, real, date, uuid, type AnyPgColumn, customType } from "drizzle-orm/pg-core";
import { pgTable, text, varchar, integer, boolean, timestamp, jsonb, serial, unique, foreignKey, uniqueIndex, index, numeric, real, date, uuid, type AnyPgColumn, customType } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { z } from "zod";
@@ -3967,142 +3967,3 @@ export const medBotBindings = pgTable("med_bot_bindings", {
export type MedBotBinding = typeof medBotBindings.$inferSelect;
export type InsertMedBotBinding = typeof medBotBindings.$inferInsert;
// =====================
// GPS-мониторинг (объекты, треки, геозоны)
// =====================
// Объекты мониторинга: транспорт ('vehicle') или люди ('person')
export const gpsAssets = pgTable("gps_assets", {
id: serial("id").primaryKey(),
organizationId: integer("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
name: text("name").notNull(),
type: text("type").$type<"vehicle" | "person">().notNull().default("vehicle"),
userId: integer("user_id").references(() => users.id),
externalId: text("external_id"), // IMEI/идентификатор трекера (уникален в рамках организации)
color: text("color").default("#3b82f6"),
isActive: boolean("is_active").default(true),
isOnline: boolean("is_online").default(false),
lastLat: doublePrecision("last_lat"),
lastLng: doublePrecision("last_lng"),
lastSpeed: doublePrecision("last_speed"),
lastCourse: doublePrecision("last_course"),
lastSeenAt: timestamp("last_seen_at"),
createdAt: timestamp("created_at").defaultNow(),
}, (table) => ({
orgIdx: index("gps_assets_org_idx").on(table.organizationId),
orgExternalIdx: uniqueIndex("gps_assets_org_external_idx").on(table.organizationId, table.externalId),
userIdx: index("gps_assets_user_idx").on(table.userId),
}));
export type GpsAsset = typeof gpsAssets.$inferSelect;
export type InsertGpsAsset = typeof gpsAssets.$inferInsert;
// История позиций (треки)
export const gpsPositions = pgTable("gps_positions", {
id: bigserial("id", { mode: "number" }).primaryKey(),
assetId: integer("asset_id").notNull().references(() => gpsAssets.id, { onDelete: "cascade" }),
organizationId: integer("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
lat: doublePrecision("lat").notNull(),
lng: doublePrecision("lng").notNull(),
speed: doublePrecision("speed"),
course: doublePrecision("course"),
accuracy: doublePrecision("accuracy"),
recordedAt: timestamp("recorded_at").notNull(),
createdAt: timestamp("created_at").defaultNow(),
}, (table) => ({
assetRecordedIdx: index("gps_positions_asset_recorded_idx").on(table.assetId, table.recordedAt),
orgRecordedIdx: index("gps_positions_org_recorded_idx").on(table.organizationId, table.recordedAt),
}));
export type GpsPosition = typeof gpsPositions.$inferSelect;
export type InsertGpsPosition = typeof gpsPositions.$inferInsert;
// Геозоны (полигоны; polygon — массив пар [lat, lng])
export const gpsGeozones = pgTable("gps_geozones", {
id: serial("id").primaryKey(),
organizationId: integer("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
name: text("name").notNull(),
polygon: jsonb("polygon").$type<Array<[number, number]>>().notNull(),
color: text("color").default("#f59e0b"),
notifyEnter: boolean("notify_enter").default(true),
notifyExit: boolean("notify_exit").default(true),
isActive: boolean("is_active").default(true),
createdAt: timestamp("created_at").defaultNow(),
}, (table) => ({
orgIdx: index("gps_geozones_org_idx").on(table.organizationId),
}));
export type GpsGeozone = typeof gpsGeozones.$inferSelect;
export type InsertGpsGeozone = typeof gpsGeozones.$inferInsert;
// События входа/выхода из геозон
export const gpsGeozoneEvents = pgTable("gps_geozone_events", {
id: bigserial("id", { mode: "number" }).primaryKey(),
organizationId: integer("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
geozoneId: integer("geozone_id").notNull().references(() => gpsGeozones.id, { onDelete: "cascade" }),
assetId: integer("asset_id").notNull().references(() => gpsAssets.id, { onDelete: "cascade" }),
event: text("event").$type<"enter" | "exit">().notNull(),
lat: doublePrecision("lat"),
lng: doublePrecision("lng"),
createdAt: timestamp("created_at").defaultNow(),
}, (table) => ({
assetIdx: index("gps_geozone_events_asset_idx").on(table.assetId, table.createdAt),
orgIdx: index("gps_geozone_events_org_idx").on(table.organizationId, table.createdAt),
}));
export type GpsGeozoneEvent = typeof gpsGeozoneEvents.$inferSelect;
export type InsertGpsGeozoneEvent = typeof gpsGeozoneEvents.$inferInsert;
// Текущее состояние «объект внутри геозоны» (для детекта входа/выхода)
export const gpsGeozoneStates = pgTable("gps_geozone_states", {
assetId: integer("asset_id").notNull().references(() => gpsAssets.id, { onDelete: "cascade" }),
geozoneId: integer("geozone_id").notNull().references(() => gpsGeozones.id, { onDelete: "cascade" }),
inside: boolean("inside").notNull(),
updatedAt: timestamp("updated_at").defaultNow(),
}, (table) => ({
pk: uniqueIndex("gps_geozone_states_pk").on(table.assetId, table.geozoneId),
}));
export type GpsGeozoneState = typeof gpsGeozoneStates.$inferSelect;
export type InsertGpsGeozoneState = typeof gpsGeozoneStates.$inferInsert;
// Группы объектов (для фильтров на карте)
export const gpsGroups = pgTable("gps_groups", {
id: serial("id").primaryKey(),
organizationId: integer("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
name: text("name").notNull(),
createdAt: timestamp("created_at").defaultNow(),
}, (table) => ({
orgIdx: index("gps_groups_org_idx").on(table.organizationId),
}));
export type GpsGroup = typeof gpsGroups.$inferSelect;
export type InsertGpsGroup = typeof gpsGroups.$inferInsert;
// Связь групп и объектов (многие-ко-многим)
export const gpsGroupAssets = pgTable("gps_group_assets", {
groupId: integer("group_id").notNull().references(() => gpsGroups.id, { onDelete: "cascade" }),
assetId: integer("asset_id").notNull().references(() => gpsAssets.id, { onDelete: "cascade" }),
}, (table) => ({
pk: uniqueIndex("gps_group_assets_pk").on(table.groupId, table.assetId),
}));
export type GpsGroupAsset = typeof gpsGroupAssets.$inferSelect;
export type InsertGpsGroupAsset = typeof gpsGroupAssets.$inferInsert;
// Подписчики на события объекта
export const gpsAssetSubscribers = pgTable("gps_asset_subscribers", {
id: serial("id").primaryKey(),
assetId: integer("asset_id").notNull().references(() => gpsAssets.id, { onDelete: "cascade" }),
userId: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
onEnter: boolean("on_enter").default(true),
onExit: boolean("on_exit").default(true),
onOffline: boolean("on_offline").default(false),
}, (table) => ({
assetUserIdx: uniqueIndex("gps_asset_subscribers_asset_user_idx").on(table.assetId, table.userId),
}));
export type GpsAssetSubscriber = typeof gpsAssetSubscribers.$inferSelect;
export type InsertGpsAssetSubscriber = typeof gpsAssetSubscribers.$inferInsert;