diff --git a/bridge/bridge_1c.py b/bridge/bridge_1c.py index b8dd492..8a6c910 100644 --- a/bridge/bridge_1c.py +++ b/bridge/bridge_1c.py @@ -385,8 +385,8 @@ class BridgeHandler(BaseHTTPRequestHandler): Слот.Вставить("doctor_id", IDВрача); Слот.Вставить("branch_id", IDФилиала); Слот.Вставить("cabinet", ИмяКабинета); - Слот.Вставить("start", Формат(Текущий, "ДФ=yyyy-MM-ddTHH:mm:ssZ")); - Слот.Вставить("end", Формат(СлотКонец, "ДФ=yyyy-MM-ddTHH:mm:ssZ")); + Слот.Вставить("start", Формат(Текущий, "ДФ=yyyy-MM-ddTHH:mm:ss") + "+03:00"); + Слот.Вставить("end", Формат(СлотКонец, "ДФ=yyyy-MM-ddTHH:mm:ss") + "+03:00"); Слот.Вставить("status", ?(Занят, "busy", "free")); Слот.Вставить("slot_id", IDВрача + "_" + Формат(Текущий, "ДФ=yyyyMMddHHmmss")); Слоты.Добавить(Слот); @@ -420,8 +420,8 @@ class BridgeHandler(BaseHTTPRequestHandler): ИначеЕсли ДокСсылка.Состояние.ТипСостояния = Перечисления.ТипыСостоянийЗаявок.Отменена Тогда Ответ.Вставить("status", "cancelled"); КонецЕсли; - Ответ.Вставить("start", Формат(ДокСсылка.ДатаНачала, "ДФ=yyyy-MM-ddTHH:mm:ssZ")); - Ответ.Вставить("end", Формат(ДокСсылка.ДатаОкончания, "ДФ=yyyy-MM-ddTHH:mm:ssZ")); + Ответ.Вставить("start", Формат(ДокСсылка.ДатаНачала, "ДФ=yyyy-MM-ddTHH:mm:ss") + "+03:00"); + Ответ.Вставить("end", Формат(ДокСсылка.ДатаОкончания, "ДФ=yyyy-MM-ddTHH:mm:ss") + "+03:00"); КонецЕсли; ЗаписьJSON = Новый ЗаписьJSON; @@ -626,8 +626,8 @@ class BridgeHandler(BaseHTTPRequestHandler): Ответ = Новый Структура; Ответ.Вставить("appointment_id", ID); Ответ.Вставить("status", "booked"); - Ответ.Вставить("start", Формат(Док.ДатаНачала, "ДФ=yyyy-MM-ddTHH:mm:ssZ")); - Ответ.Вставить("end", Формат(Док.ДатаОкончания, "ДФ=yyyy-MM-ddTHH:mm:ssZ")); + Ответ.Вставить("start", Формат(Док.ДатаНачала, "ДФ=yyyy-MM-ddTHH:mm:ss") + "+03:00"); + Ответ.Вставить("end", Формат(Док.ДатаОкончания, "ДФ=yyyy-MM-ddTHH:mm:ss") + "+03:00"); Ответ.Вставить("branch_id", НРег(XMLСтрока(ФилиалСсылка))); Ответ.Вставить("patient_id", НРег(XMLСтрока(КлиентСсылка))); diff --git a/internal/storage/onec.go b/internal/storage/onec.go index 34a2282..53e509e 100644 --- a/internal/storage/onec.go +++ b/internal/storage/onec.go @@ -10,6 +10,8 @@ import ( "dana/internal/onec" ) +var moscow = time.FixedZone("Europe/Moscow", 3*60*60) + // ---------- Справочники ---------- // normName приводит ФИО к виду для сравнения: без регистра, «ё» = «е», один пробел. @@ -262,6 +264,8 @@ func (s *Storage) FreeOnecSlots(ctx context.Context, doctorOnecID string, from, if err := rows.Scan(&x.Start, &x.End, &x.BranchID, &x.Branch, &x.Cabinet, &x.SlotID); err != nil { return nil, err } + x.Start = x.Start.In(moscow) + x.End = x.End.In(moscow) x.Status = "free" out = append(out, x) } diff --git a/web/src/components/LeadModal.jsx b/web/src/components/LeadModal.jsx index 4cfc9aa..da5740a 100644 --- a/web/src/components/LeadModal.jsx +++ b/web/src/components/LeadModal.jsx @@ -303,23 +303,26 @@ export default function LeadModal({ form = 'appointment', doctor, slot, service, {values.date ? (
- {slots.map((t) => { - const past = isPast(t); - const on = values.time === t; - return ( - - ); - })} + {slots + .filter((t) => !isPast(t)) + .map((t) => { + const on = values.time === t; + return ( + + ); + })} + {values.date === todayISO() && slots.filter((t) => !isPast(t)).length === 0 && ( +

На сегодня свободного времени уже нет. Пожалуйста, выберите другую дату.

+ )}
) : (

Сначала выберите дату — покажем время приёма

diff --git a/web/src/components/NativeBooking.jsx b/web/src/components/NativeBooking.jsx index 099c377..bbe0460 100644 --- a/web/src/components/NativeBooking.jsx +++ b/web/src/components/NativeBooking.jsx @@ -3,7 +3,7 @@ import { Link } from 'react-router-dom'; import checkIcon from '../assets/figma/icon-check.svg'; import chevronIcon from '../assets/figma/icon-accordion-chevron.svg'; import { api } from '../api.js'; -import { dayLabel, groupByDay, longDate, newRequestId, slotDate, slotTime } from '../lib/slots.js'; +import { dayLabel, groupByDay, longDate, newRequestId, slotDate, slotTime, todayKey } from '../lib/slots.js'; import { formatPhoneInput, phoneComplete } from '../lib/phone.js'; import './LeadModal.css'; import './NativeBooking.css'; @@ -25,8 +25,14 @@ export function NextSlots({ doctor, onPick, limit = 6 }) { }, [enabled, doctor.slug]); if (!enabled || !state || !state.slots?.length) return null; - const today = slotDate(state.slots[0]); - const shown = state.slots.slice(0, limit); + const today = todayKey(); + const now = Date.now(); + const validSlots = state.slots.filter((s) => { + const t = new Date(s.start).getTime(); + return isNaN(t) || t > now; + }); + if (!validSlots.length) return null; + const shown = validSlots.slice(0, limit); return (

Ближайшие свободные окна

@@ -89,9 +95,16 @@ export default function NativeBooking({ doctor, initialSlot, onClose, onUnavaila return [...m.entries()]; }, [slots]); - const visible = useMemo(() => (branch ? slots.filter((s) => s.branch_id === branch) : slots), [slots, branch]); + const visible = useMemo(() => { + const now = Date.now(); + return slots.filter((s) => { + if (branch && s.branch_id !== branch) return false; + const t = new Date(s.start).getTime(); + return isNaN(t) || t > now; + }); + }, [slots, branch]); const days = useMemo(() => groupByDay(visible), [visible]); - const today = slots[0] ? slotDate(slots[0]) : ''; + const today = todayKey(); const activeDay = days.find((d) => d.date === day) || days[0]; useEffect(() => { diff --git a/web/src/lib/slots.js b/web/src/lib/slots.js index 3ad1032..6c3c2e6 100644 --- a/web/src/lib/slots.js +++ b/web/src/lib/slots.js @@ -11,12 +11,29 @@ function parts(dateKey) { return { y, m, d, weekday: new Date(Date.UTC(y, m - 1, d)).getUTCDay() }; } -// «Сегодня» — дата первого окна в ответе: сервер отдаёт окна начиная с текущего момента по Москве. -export function dayLabel(dateKey, todayKey) { - if (dateKey === todayKey) return 'Сегодня'; +// Текущая календарная дата по времени клиники (Москва / Махачкала, UTC+3) +export function todayKey() { + try { + return new Intl.DateTimeFormat('en-CA', { + timeZone: 'Europe/Moscow', + year: 'numeric', + month: '2-digit', + day: '2-digit', + }).format(new Date()); + } catch (e) { + const d = new Date(); + d.setMinutes(d.getMinutes() - d.getTimezoneOffset()); + return d.toISOString().slice(0, 10); + } +} + +// «Сегодня», «Завтра» или день недели + дата относительно реального сегодня +export function dayLabel(dateKey, today) { + const currentToday = today || todayKey(); + if (dateKey === currentToday) return 'Сегодня'; const { y, m, d, weekday } = parts(dateKey); const prev = new Date(Date.UTC(y, m - 1, d - 1)).toISOString().slice(0, 10); - if (prev === todayKey) return 'Завтра'; + if (prev === currentToday) return 'Завтра'; return `${WEEKDAYS[weekday]}, ${d} ${MONTHS[m - 1].slice(0, 3)}`; }