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

View File

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

View File

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