diff --git a/frontend/src/pages/AlertsPage.jsx b/frontend/src/pages/AlertsPage.jsx
index 6eb3d57..725df3f 100644
--- a/frontend/src/pages/AlertsPage.jsx
+++ b/frontend/src/pages/AlertsPage.jsx
@@ -380,16 +380,16 @@ function AlertCard({ alert, index, onEdit, onDelete }) {
{alert.start_time && (
- Начало: {new Date(alert.start_time).toLocaleString('ru-RU')}
+ Начало: {new Date(alert.start_time).toLocaleString('ru-RU', { timeZone: MINSK_TIME_ZONE })}
)}
{alert.end_time && (
- Конец: {new Date(alert.end_time).toLocaleString('ru-RU')}
+ Конец: {new Date(alert.end_time).toLocaleString('ru-RU', { timeZone: MINSK_TIME_ZONE })}
)}
{status === 'scheduled' &&
Push будет отправлен в момент начала
}
- {alert.notification_sent_at &&
Push отправлен: {new Date(alert.notification_sent_at).toLocaleString('ru-RU')}
}
+ {alert.notification_sent_at &&
Push отправлен: {new Date(alert.notification_sent_at).toLocaleString('ru-RU', { timeZone: MINSK_TIME_ZONE })}
}
@@ -416,11 +416,30 @@ function createAlertIdempotencyKey() {
return `alert-${Date.now()}-${Math.random().toString(36).slice(2)}`;
}
+const MINSK_TIME_ZONE = 'Europe/Minsk';
+const MINSK_OFFSET = '+03:00';
+
function toDateTimeInput(value = new Date()) {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '';
- const local = new Date(date.getTime() - date.getTimezoneOffset() * 60_000);
- return local.toISOString().slice(0, 16);
+ const parts = new Intl.DateTimeFormat('en-CA', {
+ timeZone: MINSK_TIME_ZONE,
+ year: 'numeric', month: '2-digit', day: '2-digit',
+ hour: '2-digit', minute: '2-digit', hourCycle: 'h23'
+ }).formatToParts(date).reduce((result, part) => ({ ...result, [part.type]: part.value }), {});
+ return `${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}`;
+}
+
+// PostgreSQL currently stores alert timestamps without a timezone. Keep UTC
+// values there, while the operator always sees and enters Polotsk time.
+function toApiDateTime(value) {
+ if (!value) return null;
+ const date = new Date(`${value}:00${MINSK_OFFSET}`);
+ return Number.isNaN(date.getTime()) ? value : date.toISOString().slice(0, 19);
+}
+
+function toMinskDate(value) {
+ return new Date(`${value}:00${MINSK_OFFSET}`);
}
function AlertForm({ alert, routes, templates, onSubmit, onCancel }) {
@@ -443,7 +462,8 @@ function AlertForm({ alert, routes, templates, onSubmit, onCancel }) {
const data = {
...formData,
route_id: formData.route_id || null,
- end_time: formData.end_time || null
+ start_time: toApiDateTime(formData.start_time),
+ end_time: toApiDateTime(formData.end_time)
};
setSubmitting(true);
@@ -472,7 +492,7 @@ function AlertForm({ alert, routes, templates, onSubmit, onCancel }) {
}));
};
- const startDate = new Date(formData.start_time);
+ const startDate = toMinskDate(formData.start_time);
const isScheduled = formData.is_active && !Number.isNaN(startDate.getTime()) && startDate > new Date();
const saveLabel = alert ? 'Сохранить' : !formData.is_active ? 'Сохранить черновик' : isScheduled ? 'Запланировать' : 'Опубликовать';