fix(autocall,client): рабочие дни из графика админки — самовывоз пн-пт у робота и ссылки, доставка в ссылке читает график; тексты панели «Рабочие дни»
This commit is contained in:
parent
270ab39abc
commit
794c3ce5e3
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue