fix(slots): fix today date detection, hide past slots, format 1C timezone as +03:00
This commit is contained in:
parent
e06d8a0de5
commit
ff95ff9138
@ -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Строка(КлиентСсылка)));
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -303,23 +303,26 @@ export default function LeadModal({ form = 'appointment', doctor, slot, service,
|
||||
</span>
|
||||
{values.date ? (
|
||||
<div className="lead-slots" role="radiogroup" aria-labelledby={`${uid}-time`}>
|
||||
{slots.map((t) => {
|
||||
const past = isPast(t);
|
||||
const on = values.time === t;
|
||||
return (
|
||||
<button
|
||||
key={t}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={on}
|
||||
disabled={past}
|
||||
className={`lead-slot${on ? ' lead-slot--on' : ''}`}
|
||||
onClick={() => setValues((p) => ({ ...p, time: on ? '' : t }))}
|
||||
>
|
||||
{t}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{slots
|
||||
.filter((t) => !isPast(t))
|
||||
.map((t) => {
|
||||
const on = values.time === t;
|
||||
return (
|
||||
<button
|
||||
key={t}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={on}
|
||||
className={`lead-slot${on ? ' lead-slot--on' : ''}`}
|
||||
onClick={() => setValues((p) => ({ ...p, time: on ? '' : t }))}
|
||||
>
|
||||
{t}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{values.date === todayISO() && slots.filter((t) => !isPast(t)).length === 0 && (
|
||||
<p className="lead-slots__hint">На сегодня свободного времени уже нет. Пожалуйста, выберите другую дату.</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="lead-slots__hint">Сначала выберите дату — покажем время приёма</p>
|
||||
|
||||
@ -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 (
|
||||
<div className="next-slots">
|
||||
<p className="next-slots__title">Ближайшие свободные окна</p>
|
||||
@ -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(() => {
|
||||
|
||||
@ -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)}`;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user