fix(autocall,client): рабочие дни из графика админки — самовывоз пн-пт у робота и ссылки, доставка в ссылке читает график; тексты панели «Рабочие дни»

This commit is contained in:
Hermes 2026-09-11 15:53:03 +02:00
parent 270ab39abc
commit 794c3ce5e3
3 changed files with 81 additions and 32 deletions

View File

@ -153,6 +153,8 @@ export const BusinessSchedulePanel = () => {
<h1 className="mt-1 text-2xl font-bold leading-tight">Рабочие дни</h1> <h1 className="mt-1 text-2xl font-bold leading-tight">Рабочие дни</h1>
<p className="mt-1 text-sm text-[var(--color-text-muted)]"> <p className="mt-1 text-sm text-[var(--color-text-muted)]">
Настройте, в какие дни доступны доставка, самовывоз и отправка SMS-приглашений. Настройте, в какие дни доступны доставка, самовывоз и отправка SMS-приглашений.
Дни самовывоза — единый источник правды: их же использует страница клиента по ссылке
и робот-автодозвон, в другие дни самовывоз не предлагается.
</p> </p>
</div> </div>
{savedAt && !hasChanges && ( {savedAt && !hasChanges && (
@ -169,7 +171,8 @@ export const BusinessSchedulePanel = () => {
Доставка Доставка
</h2> </h2>
<p className="text-xs text-[var(--color-text-muted)]"> <p className="text-xs text-[var(--color-text-muted)]">
Дни, в которые клиент может выбрать доставку. В остальные дни доставка не предлагается. Дни, в которые клиент может выбрать доставку по ссылке. В остальные дни доставка
не предлагается.
</p> </p>
</div> </div>
</div> </div>
@ -191,7 +194,8 @@ export const BusinessSchedulePanel = () => {
Самовывоз Самовывоз
</h2> </h2>
<p className="text-xs text-[var(--color-text-muted)]"> <p className="text-xs text-[var(--color-text-muted)]">
Дни, в которые клиент может выбрать самовывоз. В остальные дни самовывоз не предлагается. Дни, в которые склад выдаёт самовывоз. В остальные дни самовывоз не предлагается —
ни в ссылке клиента, ни в звонке робота.
</p> </p>
</div> </div>
</div> </div>

View File

@ -15,6 +15,7 @@ import { formatDeliveryDate } from "../components/client/deliveryDateFormatting"
import { import {
confirmDeliveryChoice, confirmDeliveryChoice,
fetchDeliveryInvitation, fetchDeliveryInvitation,
fetchDeliveryWorkDays,
fetchPickupWorkDays, fetchPickupWorkDays,
} from "../services/deliveryInvitationApi"; } from "../services/deliveryInvitationApi";
@ -128,30 +129,52 @@ const isSundayKey = (dateKey) => {
return d.getUTCDay() === 0; return d.getUTCDay() === 0;
}; };
const getNextWorkdayKey = (dateKey) => { // Рабочие дни доставки из «Графика работы» (1=пн … 7=вс).
// Дефолт — прежнее поведение страницы: все дни, кроме воскресенья.
const DEFAULT_DELIVERY_DAYS = [1, 2, 3, 4, 5, 6];
const isAllowedDeliveryDayKey = (dateKey, workDays = DEFAULT_DELIVERY_DAYS) => {
if (!dateKey) return false;
const d = new Date(`${dateKey}T12:00:00Z`);
if (Number.isNaN(d.getTime())) return false;
const weekday = d.getUTCDay() === 0 ? 7 : d.getUTCDay();
return workDays.includes(weekday);
};
const getNextWorkdayKey = (dateKey, workDays = DEFAULT_DELIVERY_DAYS) => {
let next = addDaysToDateKey(dateKey, 1); let next = addDaysToDateKey(dateKey, 1);
while (isSundayKey(next)) { let guard = 0;
while (!isAllowedDeliveryDayKey(next, workDays) && guard < 14) {
next = addDaysToDateKey(next, 1); next = addDaysToDateKey(next, 1);
guard += 1;
} }
return next; return next;
}; };
const getAllowedDeliveryDateKeys = (referenceDate = new Date()) => { const getAllowedDeliveryDateKeys = (referenceDate = new Date(), workDays = DEFAULT_DELIVERY_DAYS) => {
const todayKey = getBusinessTodayKey(referenceDate); const todayKey = getBusinessTodayKey(referenceDate);
const firstWorkday = getNextWorkdayKey(todayKey); const firstWorkday = getNextWorkdayKey(todayKey, workDays);
const secondWorkday = getNextWorkdayKey(firstWorkday); const secondWorkday = getNextWorkdayKey(firstWorkday, workDays);
return new Set([firstWorkday, secondWorkday].filter(Boolean)); return new Set([firstWorkday, secondWorkday].filter(Boolean));
}; };
const isAllowedDeliverySlotDate = (dateKey, referenceDate = new Date()) => { const isAllowedDeliverySlotDate = (
dateKey,
referenceDate = new Date(),
workDays = DEFAULT_DELIVERY_DAYS,
) => {
if (!dateKey) { if (!dateKey) {
return false; return false;
} }
return getAllowedDeliveryDateKeys(referenceDate).has(dateKey); return getAllowedDeliveryDateKeys(referenceDate, workDays).has(dateKey);
}; };
export const groupSlotsFromInvitation = (invitation, referenceDate = new Date()) => { export const groupSlotsFromInvitation = (
invitation,
referenceDate = new Date(),
workDays = DEFAULT_DELIVERY_DAYS,
) => {
if (!invitation) { if (!invitation) {
return []; return [];
} }
@ -161,10 +184,10 @@ export const groupSlotsFromInvitation = (invitation, referenceDate = new Date())
const deliveryTime = invitation.deliveryTime; const deliveryTime = invitation.deliveryTime;
if (!rawSlots.length && !deliveryDate) { if (!rawSlots.length && !deliveryDate) {
// Fallback: generate default delivery slots (next 2 workdays, both halves, skip Sunday) // Fallback: generate default delivery slots (next 2 workdays, both halves)
const todayKey = getBusinessTodayKey(referenceDate); const todayKey = getBusinessTodayKey(referenceDate);
const firstWorkday = getNextWorkdayKey(todayKey); const firstWorkday = getNextWorkdayKey(todayKey, workDays);
const secondWorkday = getNextWorkdayKey(firstWorkday); const secondWorkday = getNextWorkdayKey(firstWorkday, workDays);
return [ return [
{ id: `slot-${firstWorkday}-first`, date: firstWorkday, time: "Первая половина дня" }, { id: `slot-${firstWorkday}-first`, date: firstWorkday, time: "Первая половина дня" },
{ id: `slot-${firstWorkday}-second`, date: firstWorkday, time: "Вторая половина дня" }, { id: `slot-${firstWorkday}-second`, date: firstWorkday, time: "Вторая половина дня" },
@ -194,7 +217,7 @@ export const groupSlotsFromInvitation = (invitation, referenceDate = new Date())
|| deliveryDate || deliveryDate
|| ""; || "";
if (!isAllowedDeliverySlotDate(parsedDate, referenceDate)) { if (!isAllowedDeliverySlotDate(parsedDate, referenceDate, workDays)) {
return null; return null;
} }
@ -218,7 +241,7 @@ export const groupSlotsFromInvitation = (invitation, referenceDate = new Date())
return null; return null;
} }
if (!isAllowedDeliverySlotDate(slotDate, referenceDate)) { if (!isAllowedDeliverySlotDate(slotDate, referenceDate, workDays)) {
return null; return null;
} }
@ -306,10 +329,16 @@ export const ClientDeliveryPage = () => {
[token, invitation?.smsSentAt], [token, invitation?.smsSentAt],
); );
// Рабочие дни самовывоза — из графика компании, те же, что называет робот. // Рабочие дни — из графика компании, те же, что называет робот.
const [pickupWorkDays, setPickupWorkDays] = React.useState([1, 2, 3, 4, 5]); const [pickupWorkDays, setPickupWorkDays] = React.useState([1, 2, 3, 4, 5]);
const [deliveryWorkDays, setDeliveryWorkDays] = React.useState(DEFAULT_DELIVERY_DAYS);
React.useEffect(() => { React.useEffect(() => {
let cancelled = false; let cancelled = false;
fetchDeliveryWorkDays()
.then((days) => {
if (!cancelled) setDeliveryWorkDays(days);
})
.catch(() => {});
fetchPickupWorkDays() fetchPickupWorkDays()
.then((days) => { .then((days) => {
if (!cancelled) setPickupWorkDays(days); if (!cancelled) setPickupWorkDays(days);
@ -365,7 +394,7 @@ export const ClientDeliveryPage = () => {
}; };
}, [token]); }, [token]);
const slots = groupSlotsFromInvitation(invitation, referenceDate); const slots = groupSlotsFromInvitation(invitation, referenceDate, deliveryWorkDays);
const invitationState = invitation?.state || "awaiting_choice"; const invitationState = invitation?.state || "awaiting_choice";
const isActiveState = ["awaiting_choice", "opened", "reminder_sent"].includes(invitationState); const isActiveState = ["awaiting_choice", "opened", "reminder_sent"].includes(invitationState);
@ -412,7 +441,7 @@ export const ClientDeliveryPage = () => {
setSelectedSlot( setSelectedSlot(
buildSelectedSlotFromInvitation( buildSelectedSlotFromInvitation(
loadedInvitation, loadedInvitation,
groupSlotsFromInvitation(loadedInvitation, referenceDate), groupSlotsFromInvitation(loadedInvitation, referenceDate, deliveryWorkDays),
) || effectiveSelectedSlot, ) || effectiveSelectedSlot,
); );
setChoiceSaved(true); setChoiceSaved(true);

View File

@ -184,30 +184,46 @@ export const __resetLocalDeliveryInvitationCache = () => {
}; };
/** /**
* Рабочие дни самовывоза из графика компании («График работы»). * Рабочие дни из графика компании («График работы» в админке).
* Робот и страница клиента обязаны предлагать одни и те же дни, иначе * Админка — единственный источник правды: и страница клиента, и робот-автодозвон
* клиент выберет день, который склад не работает (например, воскресенье). * обязаны предлагать одни и те же дни, иначе клиент выберет день, когда склад
* Номера дней: 1=пн … 7=вс. * или доставка не работают. Номера дней: 1=пн … 7=вс.
*/ */
export const fetchPickupWorkDays = async () => { const SCHEDULE_FALLBACK = {
const fallback = [1, 2, 3, 4, 5]; deliveryDays: [1, 2, 3, 4, 5, 6],
pickupDays: [1, 2, 3, 4, 5],
smsDays: [1, 2, 3, 4, 5],
};
const normalizeDays = (days, fallback) => {
const parsed = (Array.isArray(days) ? days : [])
.map((d) => Number(d))
.filter((d) => Number.isInteger(d) && d >= 1 && d <= 7);
return parsed.length ? parsed : fallback;
};
export const fetchBusinessSchedule = async () => {
if (!hasSupabaseConfig || !supabase?.rpc) { if (!hasSupabaseConfig || !supabase?.rpc) {
return fallback; return { ...SCHEDULE_FALLBACK };
} }
try { try {
const { data, error } = await supabase.rpc("get_business_schedule"); const { data, error } = await supabase.rpc("get_business_schedule");
if (error) throw error; if (error) throw error;
const days = Array.isArray(data?.pickupDays) ? data.pickupDays : []; return {
const parsed = days deliveryDays: normalizeDays(data?.deliveryDays, SCHEDULE_FALLBACK.deliveryDays),
.map((d) => Number(d)) pickupDays: normalizeDays(data?.pickupDays, SCHEDULE_FALLBACK.pickupDays),
.filter((d) => Number.isInteger(d) && d >= 1 && d <= 7); smsDays: normalizeDays(data?.smsDays, SCHEDULE_FALLBACK.smsDays),
return parsed.length ? parsed : fallback; };
} catch (e) { } catch (e) {
console.warn("Не удалось получить график самовывоза, беру пн-пт", e); console.warn("Не удалось получить график работы, беру значения по умолчанию", e);
return fallback; return { ...SCHEDULE_FALLBACK };
} }
}; };
export const fetchPickupWorkDays = async () => (await fetchBusinessSchedule()).pickupDays;
export const fetchDeliveryWorkDays = async () => (await fetchBusinessSchedule()).deliveryDays;
export const fetchDeliveryInvitation = async (token) => { export const fetchDeliveryInvitation = async (token) => {
if (!token) { if (!token) {
throw new Error("Token is required"); throw new Error("Token is required");