fix(slots): fix today date detection, hide past slots, format 1C timezone as +03:00

This commit is contained in:
Dana Medical Developer 2026-10-14 15:06:46 +00:00
parent e06d8a0de5
commit ff95ff9138
5 changed files with 69 additions and 32 deletions

View File

@ -385,8 +385,8 @@ class BridgeHandler(BaseHTTPRequestHandler):
Слот.Вставить("doctor_id", IDВрача); Слот.Вставить("doctor_id", IDВрача);
Слот.Вставить("branch_id", IDФилиала); Слот.Вставить("branch_id", IDФилиала);
Слот.Вставить("cabinet", ИмяКабинета); Слот.Вставить("cabinet", ИмяКабинета);
Слот.Вставить("start", Формат(Текущий, "ДФ=yyyy-MM-ddTHH:mm:ssZ")); Слот.Вставить("start", Формат(Текущий, "ДФ=yyyy-MM-ddTHH:mm:ss") + "+03:00");
Слот.Вставить("end", Формат(СлотКонец, "ДФ=yyyy-MM-ddTHH:mm:ssZ")); Слот.Вставить("end", Формат(СлотКонец, "ДФ=yyyy-MM-ddTHH:mm:ss") + "+03:00");
Слот.Вставить("status", ?(Занят, "busy", "free")); Слот.Вставить("status", ?(Занят, "busy", "free"));
Слот.Вставить("slot_id", IDВрача + "_" + Формат(Текущий, "ДФ=yyyyMMddHHmmss")); Слот.Вставить("slot_id", IDВрача + "_" + Формат(Текущий, "ДФ=yyyyMMddHHmmss"));
Слоты.Добавить(Слот); Слоты.Добавить(Слот);
@ -420,8 +420,8 @@ class BridgeHandler(BaseHTTPRequestHandler):
ИначеЕсли ДокСсылка.Состояние.ТипСостояния = Перечисления.ТипыСостоянийЗаявок.Отменена Тогда ИначеЕсли ДокСсылка.Состояние.ТипСостояния = Перечисления.ТипыСостоянийЗаявок.Отменена Тогда
Ответ.Вставить("status", "cancelled"); Ответ.Вставить("status", "cancelled");
КонецЕсли; КонецЕсли;
Ответ.Вставить("start", Формат(ДокСсылка.ДатаНачала, "ДФ=yyyy-MM-ddTHH:mm:ssZ")); Ответ.Вставить("start", Формат(ДокСсылка.ДатаНачала, "ДФ=yyyy-MM-ddTHH:mm:ss") + "+03:00");
Ответ.Вставить("end", Формат(ДокСсылка.ДатаОкончания, "ДФ=yyyy-MM-ddTHH:mm:ssZ")); Ответ.Вставить("end", Формат(ДокСсылка.ДатаОкончания, "ДФ=yyyy-MM-ddTHH:mm:ss") + "+03:00");
КонецЕсли; КонецЕсли;
ЗаписьJSON = Новый ЗаписьJSON; ЗаписьJSON = Новый ЗаписьJSON;
@ -626,8 +626,8 @@ class BridgeHandler(BaseHTTPRequestHandler):
Ответ = Новый Структура; Ответ = Новый Структура;
Ответ.Вставить("appointment_id", ID); Ответ.Вставить("appointment_id", ID);
Ответ.Вставить("status", "booked"); Ответ.Вставить("status", "booked");
Ответ.Вставить("start", Формат(Док.ДатаНачала, "ДФ=yyyy-MM-ddTHH:mm:ssZ")); Ответ.Вставить("start", Формат(Док.ДатаНачала, "ДФ=yyyy-MM-ddTHH:mm:ss") + "+03:00");
Ответ.Вставить("end", Формат(Док.ДатаОкончания, "ДФ=yyyy-MM-ddTHH:mm:ssZ")); Ответ.Вставить("end", Формат(Док.ДатаОкончания, "ДФ=yyyy-MM-ddTHH:mm:ss") + "+03:00");
Ответ.Вставить("branch_id", НРег(XMLСтрока(ФилиалСсылка))); Ответ.Вставить("branch_id", НРег(XMLСтрока(ФилиалСсылка)));
Ответ.Вставить("patient_id", НРег(XMLСтрока(КлиентСсылка))); Ответ.Вставить("patient_id", НРег(XMLСтрока(КлиентСсылка)));

View File

@ -10,6 +10,8 @@ import (
"dana/internal/onec" "dana/internal/onec"
) )
var moscow = time.FixedZone("Europe/Moscow", 3*60*60)
// ---------- Справочники ---------- // ---------- Справочники ----------
// normName приводит ФИО к виду для сравнения: без регистра, «ё» = «е», один пробел. // 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 { if err := rows.Scan(&x.Start, &x.End, &x.BranchID, &x.Branch, &x.Cabinet, &x.SlotID); err != nil {
return nil, err return nil, err
} }
x.Start = x.Start.In(moscow)
x.End = x.End.In(moscow)
x.Status = "free" x.Status = "free"
out = append(out, x) out = append(out, x)
} }

View File

@ -303,8 +303,9 @@ export default function LeadModal({ form = 'appointment', doctor, slot, service,
</span> </span>
{values.date ? ( {values.date ? (
<div className="lead-slots" role="radiogroup" aria-labelledby={`${uid}-time`}> <div className="lead-slots" role="radiogroup" aria-labelledby={`${uid}-time`}>
{slots.map((t) => { {slots
const past = isPast(t); .filter((t) => !isPast(t))
.map((t) => {
const on = values.time === t; const on = values.time === t;
return ( return (
<button <button
@ -312,7 +313,6 @@ export default function LeadModal({ form = 'appointment', doctor, slot, service,
type="button" type="button"
role="radio" role="radio"
aria-checked={on} aria-checked={on}
disabled={past}
className={`lead-slot${on ? ' lead-slot--on' : ''}`} className={`lead-slot${on ? ' lead-slot--on' : ''}`}
onClick={() => setValues((p) => ({ ...p, time: on ? '' : t }))} onClick={() => setValues((p) => ({ ...p, time: on ? '' : t }))}
> >
@ -320,6 +320,9 @@ export default function LeadModal({ form = 'appointment', doctor, slot, service,
</button> </button>
); );
})} })}
{values.date === todayISO() && slots.filter((t) => !isPast(t)).length === 0 && (
<p className="lead-slots__hint">На сегодня свободного времени уже нет. Пожалуйста, выберите другую дату.</p>
)}
</div> </div>
) : ( ) : (
<p className="lead-slots__hint">Сначала выберите дату — покажем время приёма</p> <p className="lead-slots__hint">Сначала выберите дату — покажем время приёма</p>

View File

@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
import checkIcon from '../assets/figma/icon-check.svg'; import checkIcon from '../assets/figma/icon-check.svg';
import chevronIcon from '../assets/figma/icon-accordion-chevron.svg'; import chevronIcon from '../assets/figma/icon-accordion-chevron.svg';
import { api } from '../api.js'; 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 { formatPhoneInput, phoneComplete } from '../lib/phone.js';
import './LeadModal.css'; import './LeadModal.css';
import './NativeBooking.css'; import './NativeBooking.css';
@ -25,8 +25,14 @@ export function NextSlots({ doctor, onPick, limit = 6 }) {
}, [enabled, doctor.slug]); }, [enabled, doctor.slug]);
if (!enabled || !state || !state.slots?.length) return null; if (!enabled || !state || !state.slots?.length) return null;
const today = slotDate(state.slots[0]); const today = todayKey();
const shown = state.slots.slice(0, limit); 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 ( return (
<div className="next-slots"> <div className="next-slots">
<p className="next-slots__title">Ближайшие свободные окна</p> <p className="next-slots__title">Ближайшие свободные окна</p>
@ -89,9 +95,16 @@ export default function NativeBooking({ doctor, initialSlot, onClose, onUnavaila
return [...m.entries()]; return [...m.entries()];
}, [slots]); }, [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 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]; const activeDay = days.find((d) => d.date === day) || days[0];
useEffect(() => { useEffect(() => {

View File

@ -11,12 +11,29 @@ function parts(dateKey) {
return { y, m, d, weekday: new Date(Date.UTC(y, m - 1, d)).getUTCDay() }; return { y, m, d, weekday: new Date(Date.UTC(y, m - 1, d)).getUTCDay() };
} }
// «Сегодня» — дата первого окна в ответе: сервер отдаёт окна начиная с текущего момента по Москве. // Текущая календарная дата по времени клиники (Москва / Махачкала, UTC+3)
export function dayLabel(dateKey, todayKey) { export function todayKey() {
if (dateKey === todayKey) return 'Сегодня'; 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 { y, m, d, weekday } = parts(dateKey);
const prev = new Date(Date.UTC(y, m - 1, d - 1)).toISOString().slice(0, 10); 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)}`; return `${WEEKDAYS[weekday]}, ${d} ${MONTHS[m - 1].slice(0, 3)}`;
} }