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Врача);
|
Слот.Вставить("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Строка(КлиентСсылка)));
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -303,23 +303,26 @@ 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))
|
||||||
const on = values.time === t;
|
.map((t) => {
|
||||||
return (
|
const on = values.time === t;
|
||||||
<button
|
return (
|
||||||
key={t}
|
<button
|
||||||
type="button"
|
key={t}
|
||||||
role="radio"
|
type="button"
|
||||||
aria-checked={on}
|
role="radio"
|
||||||
disabled={past}
|
aria-checked={on}
|
||||||
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 }))}
|
||||||
>
|
>
|
||||||
{t}
|
{t}
|
||||||
</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>
|
||||||
|
|||||||
@ -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(() => {
|
||||||
|
|||||||
@ -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)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user