fix(number-fields): избегаем потери точности длинных чисел

- number-поля теперь рендерятся как text + inputMode=numeric,
  чтобы браузер не округлял значения через input type=number
- пробелы при вставке в number-поля удаляются
- бэкенд нормализует значения number-полей в строку перед сохранением
- добавлен хелпер normalizeFieldValueForStorage

Closes: искажение расчётного счёта и других длинных числовых полей
This commit is contained in:
2026-07-07 21:03:40 +03:00
commit 1f5ecb6da4
1089 changed files with 237546 additions and 0 deletions

View File

@@ -0,0 +1,182 @@
import { Router } from "express";
import { storage } from "../storage";
import { authenticateToken, type AuthenticatedRequest } from "../middleware/auth.middleware";
import { tenantIsolation } from "../middleware/tenant.middleware";
import { validateRequest } from "../middleware/validation.middleware";
import { createSubscriptionSchema, updateSubscriptionSchema } from "@shared/schema";
import { notificationService } from "../services/notification.service";
export function registerChatNotificationRoutes(router: Router): void {
// Get user notifications
router.get('/api/notifications',
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const { unread } = req.query;
const isReadFilter = unread === 'true' ? false : undefined;
const notifications = await storage.getUserNotifications(
req.user!.id, req.organizationId!, isReadFilter
);
res.json({ success: true, notifications });
} catch (error) {
console.error('Get notifications error:', error);
res.status(500).json({ success: false, error: 'Ошибка при загрузке уведомлений' });
}
}
);
// Mark notification as read
router.put('/api/notifications/:id/read',
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const notificationId = parseInt(req.params.id);
if (isNaN(notificationId)) {
return res.status(400).json({ success: false, error: 'Некорректный ID уведомления' });
}
const updatedNotification = await storage.markNotificationAsRead(
notificationId, req.user!.id, req.organizationId!
);
res.json({ success: true, message: 'Уведомление отмечено как прочитанное', notification: updatedNotification });
} catch (error) {
console.error('Mark notification as read error:', error);
res.status(500).json({ success: false, error: 'Ошибка при обновлении уведомления' });
}
}
);
// Mark all notifications as read
router.put('/api/notifications/read-all',
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
await storage.markAllNotificationsAsRead(req.user!.id, req.organizationId!);
res.json({ success: true, message: 'Все уведомления отмечены как прочитанные' });
} catch (error) {
console.error('Mark all notifications as read error:', error);
res.status(500).json({ success: false, error: 'Ошибка при обновлении уведомлений' });
}
}
);
// Delete notification
router.delete('/api/notifications/:id',
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const notificationId = parseInt(req.params.id);
if (isNaN(notificationId)) {
return res.status(400).json({ success: false, error: 'Некорректный ID уведомления' });
}
await storage.deleteNotification(notificationId, req.user!.id, req.organizationId!);
res.json({ success: true, message: 'Уведомление удалено' });
} catch (error) {
console.error('Delete notification error:', error);
res.status(500).json({ success: false, error: 'Ошибка при удалении уведомления' });
}
}
);
// Get all event types for subscription
router.get('/api/subscriptions/event-types',
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const eventTypes = await notificationService.getEventTypes();
res.json({ success: true, eventTypes });
} catch (error) {
console.error('Get event types error:', error);
res.status(500).json({ success: false, error: 'Ошибка при загрузке типов событий' });
}
}
);
// Get user subscriptions
router.get('/api/subscriptions',
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const subscriptions = await notificationService.getUserSubscriptions(
req.user!.id, req.organizationId!
);
res.json({ success: true, subscriptions });
} catch (error) {
console.error('Get subscriptions error:', error);
res.status(500).json({ success: false, error: 'Ошибка при загрузке подписок' });
}
}
);
// Create subscription
router.post('/api/subscriptions',
authenticateToken,
tenantIsolation,
validateRequest(createSubscriptionSchema),
async (req: AuthenticatedRequest, res) => {
try {
const subscription = await notificationService.createSubscription({
userId: req.user!.id,
organizationId: req.organizationId!,
eventTypeCode: req.body.eventTypeCode,
targetType: req.body.targetType,
targetId: req.body.targetId,
channels: req.body.channels,
});
res.status(201).json({ success: true, message: 'Подписка создана', subscription });
} catch (error) {
console.error('Create subscription error:', error);
res.status(500).json({ success: false, error: 'Ошибка при создании подписки' });
}
}
);
// Update subscription
router.patch('/api/subscriptions/:id',
authenticateToken,
tenantIsolation,
validateRequest(updateSubscriptionSchema),
async (req: AuthenticatedRequest, res) => {
try {
const subscriptionId = parseInt(req.params.id);
if (isNaN(subscriptionId)) {
return res.status(400).json({ success: false, error: 'Некорректный ID подписки' });
}
const subscription = await notificationService.updateSubscription(
subscriptionId, req.user!.id, req.organizationId!, req.body
);
if (!subscription) {
return res.status(404).json({ success: false, error: 'Подписка не найдена' });
}
res.json({ success: true, message: 'Подписка обновлена', subscription });
} catch (error) {
console.error('Update subscription error:', error);
res.status(500).json({ success: false, error: 'Ошибка при обновлении подписки' });
}
}
);
// Delete subscription
router.delete('/api/subscriptions/:id',
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const subscriptionId = parseInt(req.params.id);
if (isNaN(subscriptionId)) {
return res.status(400).json({ success: false, error: 'Некорректный ID подписки' });
}
await notificationService.deleteSubscription(subscriptionId, req.user!.id, req.organizationId!);
res.json({ success: true, message: 'Подписка удалена' });
} catch (error) {
console.error('Delete subscription error:', error);
res.status(500).json({ success: false, error: 'Ошибка при удалении подписки' });
}
}
);
}