diff --git a/scripts/sms_first_campaign.py b/scripts/sms_first_campaign.py
index f6fe219..0eb3a13 100644
--- a/scripts/sms_first_campaign.py
+++ b/scripts/sms_first_campaign.py
@@ -312,6 +312,22 @@ def update_order_group(conn, group_id, fields):
# ─── Проверка рабочего времени ────────────────────────────────────────────────
+
+def get_sms_days(conn):
+ """Read sms_days from business_schedule_settings. Returns comma-separated string."""
+ try:
+ with conn.cursor(cursor_factory=RealDictCursor) as cur:
+ cur.execute("SELECT sms_days FROM business_schedule_settings WHERE id = 1")
+ row = cur.fetchone()
+ if row and row.get("sms_days"):
+ days = row["sms_days"]
+ if isinstance(days, list):
+ return ",".join(str(d) for d in days)
+ return str(days)
+ except Exception:
+ pass
+ return "1,2,3,4,5" # Default: Mon-Fri
+
def is_within_work_hours(settings):
"""Проверка: сейчас рабочие часы.
settings: work_hours_start, work_hours_end (часы 0-23), work_days ('1,2,3,4,5')
@@ -586,6 +602,8 @@ def main():
try:
settings = load_settings(conn)
+ # Override work_days with business_schedule sms_days
+ settings["work_days"] = get_sms_days(conn)
# Update last_run_at timestamp
with conn.cursor() as cur:
diff --git a/scripts/sms_manual_campaign.py b/scripts/sms_manual_campaign.py
index 446fafe..0cb8190 100644
--- a/scripts/sms_manual_campaign.py
+++ b/scripts/sms_manual_campaign.py
@@ -76,6 +76,22 @@ def send_telegram(message, chat_id):
# ─── Проверка рабочего времени ────────────────────────────────────────────────
+
+def get_sms_days(conn):
+ """Read sms_days from business_schedule_settings. Returns comma-separated string."""
+ try:
+ with conn.cursor(cursor_factory=RealDictCursor) as cur:
+ cur.execute("SELECT sms_days FROM business_schedule_settings WHERE id = 1")
+ row = cur.fetchone()
+ if row and row.get("sms_days"):
+ days = row["sms_days"]
+ if isinstance(days, list):
+ return ",".join(str(d) for d in days)
+ return str(days)
+ except Exception:
+ pass
+ return "1,2,3,4,5" # Default: Mon-Fri
+
def is_within_work_hours(settings):
now_msk = datetime.now(timezone(timedelta(hours=3)))
today_num = now_msk.weekday() + 1
@@ -217,6 +233,8 @@ def main():
try:
settings = load_settings(conn)
+ # Override work_days with business_schedule sms_days
+ settings["work_days"] = get_sms_days(conn)
# Update last_run_at timestamp
with conn.cursor() as cur:
diff --git a/scripts/sms_paid_storage_campaign.py b/scripts/sms_paid_storage_campaign.py
index b22edaa..0d3ebd4 100644
--- a/scripts/sms_paid_storage_campaign.py
+++ b/scripts/sms_paid_storage_campaign.py
@@ -176,6 +176,22 @@ def send_telegram(message, chat_id):
# ─── Проверка рабочего времени ────────────────────────────────────────────────
+
+def get_sms_days(conn):
+ """Read sms_days from business_schedule_settings. Returns comma-separated string."""
+ try:
+ with conn.cursor(cursor_factory=RealDictCursor) as cur:
+ cur.execute("SELECT sms_days FROM business_schedule_settings WHERE id = 1")
+ row = cur.fetchone()
+ if row and row.get("sms_days"):
+ days = row["sms_days"]
+ if isinstance(days, list):
+ return ",".join(str(d) for d in days)
+ return str(days)
+ except Exception:
+ pass
+ return "1,2,3,4,5" # Default: Mon-Fri
+
def is_within_work_hours(settings):
now_msk = datetime.now(timezone(timedelta(hours=3)))
today_num = now_msk.weekday() + 1
@@ -479,6 +495,8 @@ def main():
try:
settings = load_settings(conn)
+ # Override work_days with business_schedule sms_days
+ settings["work_days"] = get_sms_days(conn)
# Update last_run_at timestamp
with conn.cursor() as cur:
diff --git a/scripts/sms_second_campaign.py b/scripts/sms_second_campaign.py
index 9e7e931..fdb3f29 100644
--- a/scripts/sms_second_campaign.py
+++ b/scripts/sms_second_campaign.py
@@ -323,6 +323,22 @@ def update_order_group(conn, group_id, fields):
# ─── Проверка рабочего времени ────────────────────────────────────────────────
+
+def get_sms_days(conn):
+ """Read sms_days from business_schedule_settings. Returns comma-separated string."""
+ try:
+ with conn.cursor(cursor_factory=RealDictCursor) as cur:
+ cur.execute("SELECT sms_days FROM business_schedule_settings WHERE id = 1")
+ row = cur.fetchone()
+ if row and row.get("sms_days"):
+ days = row["sms_days"]
+ if isinstance(days, list):
+ return ",".join(str(d) for d in days)
+ return str(days)
+ except Exception:
+ pass
+ return "1,2,3,4,5" # Default: Mon-Fri
+
def is_within_work_hours(settings):
"""Проверка: сейчас рабочие часы.
settings: work_hours_start, work_hours_end (часы 0-23), work_days ('1,2,3,4,5')
@@ -593,6 +609,8 @@ def main():
try:
settings = load_settings(conn)
+ # Override work_days with business_schedule sms_days
+ settings["work_days"] = get_sms_days(conn)
# Update last_run_at timestamp
with conn.cursor() as cur:
diff --git a/src/components/admin/BusinessSchedulePanel.jsx b/src/components/admin/BusinessSchedulePanel.jsx
new file mode 100644
index 0000000..76bc703
--- /dev/null
+++ b/src/components/admin/BusinessSchedulePanel.jsx
@@ -0,0 +1,255 @@
+/**
+ * @file BusinessSchedulePanel.jsx
+ * @description Admin panel for configuring work days: delivery, pickup, SMS.
+ * Days stored in business_schedule_settings table as INT[] (1=Mon ... 7=Sun).
+ */
+import React, { useState, useEffect, useCallback } from "react";
+import { Panel } from "../UI/Panel";
+import { Button } from "../UI/Button";
+import { Badge } from "../UI/Badge";
+import { supabase } from "../../supabaseClient";
+
+const DAY_LABELS = [
+ { num: 1, label: "Пн", full: "Понедельник" },
+ { num: 2, label: "Вт", full: "Вторник" },
+ { num: 3, label: "Ср", full: "Среда" },
+ { num: 4, label: "Чт", full: "Четверг" },
+ { num: 5, label: "Пт", full: "Пятница" },
+ { num: 6, label: "Сб", full: "Суббота" },
+ { num: 7, label: "Вс", full: "Воскресенье" },
+];
+
+const DEFAULT_SCHEDULE = {
+ deliveryDays: [1, 2, 3, 4, 5],
+ pickupDays: [1, 2, 3, 4, 5],
+ smsDays: [1, 2, 3, 4, 5],
+};
+
+const DayPills = ({ selectedDays, onToggle, accentColor }) => {
+ const toggle = (num) => {
+ if (selectedDays.includes(num)) {
+ onToggle(selectedDays.filter((d) => d !== num).sort());
+ } else {
+ onToggle([...selectedDays, num].sort());
+ }
+ };
+
+ return (
+
+ {DAY_LABELS.map((day) => {
+ const isSelected = selectedDays.includes(day.num);
+ return (
+
+ );
+ })}
+
+ );
+};
+
+export const BusinessSchedulePanel = () => {
+ const [schedule, setSchedule] = useState(DEFAULT_SCHEDULE);
+ const [original, setOriginal] = useState(DEFAULT_SCHEDULE);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isSaving, setIsSaving] = useState(false);
+ const [error, setError] = useState("");
+ const [savedAt, setSavedAt] = useState(null);
+
+ const loadSchedule = useCallback(async () => {
+ setIsLoading(true);
+ setError("");
+ try {
+ const { data, error: rpcError } = await supabase.rpc("get_business_schedule");
+ if (rpcError) throw rpcError;
+ if (data?.ok) {
+ const loaded = {
+ deliveryDays: data.deliveryDays || DEFAULT_SCHEDULE.deliveryDays,
+ pickupDays: data.pickupDays || DEFAULT_SCHEDULE.pickupDays,
+ smsDays: data.smsDays || DEFAULT_SCHEDULE.smsDays,
+ };
+ setSchedule(loaded);
+ setOriginal(loaded);
+ }
+ } catch (e) {
+ setError(e instanceof Error ? e.message : "Не удалось загрузить расписание");
+ } finally {
+ setIsLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ loadSchedule();
+ }, [loadSchedule]);
+
+ const hasChanges =
+ JSON.stringify(schedule.deliveryDays) !== JSON.stringify(original.deliveryDays) ||
+ JSON.stringify(schedule.pickupDays) !== JSON.stringify(original.pickupDays) ||
+ JSON.stringify(schedule.smsDays) !== JSON.stringify(original.smsDays);
+
+ const handleSave = async () => {
+ if (!hasChanges) return;
+
+ // Validate at least one day per category
+ if (schedule.deliveryDays.length === 0 || schedule.pickupDays.length === 0 || schedule.smsDays.length === 0) {
+ setError("Каждая категория должна иметь хотя бы один рабочий день");
+ return;
+ }
+
+ setIsSaving(true);
+ setError("");
+ try {
+ const { data, error: rpcError } = await supabase.rpc("update_business_schedule", {
+ p_delivery_days: schedule.deliveryDays,
+ p_pickup_days: schedule.pickupDays,
+ p_sms_days: schedule.smsDays,
+ });
+ if (rpcError) throw rpcError;
+ if (data?.ok) {
+ setOriginal({ ...schedule });
+ setSavedAt(new Date());
+ }
+ } catch (e) {
+ setError(e instanceof Error ? e.message : "Не удалось сохранить расписание");
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ const formatDays = (days) => {
+ return days
+ .sort((a, b) => a - b)
+ .map((d) => DAY_LABELS.find((dl) => dl.num === d)?.label)
+ .filter(Boolean)
+ .join(", ");
+ };
+
+ if (isLoading) {
+ return (
+
+ Загрузка расписания…
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
Расписание
+
Рабочие дни
+
+ Настройте, в какие дни доступны доставка, самовывоз и отправка SMS-приглашений.
+
+
+ {savedAt && !hasChanges && (
+
Сохранено
+ )}
+
+
+ {/* Delivery days */}
+
+
+
🚚
+
+
+ Доставка
+
+
+ Дни, в которые клиент может выбрать доставку. В остальные дни доставка не предлагается.
+
+
+
+ setSchedule((s) => ({ ...s, deliveryDays: days }))}
+ />
+
+ Выбрано: {formatDays(schedule.deliveryDays) || "—"}
+
+
+
+ {/* Pickup days */}
+
+
+
🏪
+
+
+ Самовывоз
+
+
+ Дни, в которые клиент может выбрать самовывоз. В остальные дни самовывоз не предлагается.
+
+
+
+ setSchedule((s) => ({ ...s, pickupDays: days }))}
+ />
+
+ Выбрано: {formatDays(schedule.pickupDays) || "—"}
+
+
+
+ {/* SMS days */}
+
+
+
📨
+
+
+ SMS-приглашения
+
+
+ Дни, в которые отправляются SMS-приглашения клиентам. В нерабочие дни SMS не отправляются (проверка статусов работает круглосуточно).
+
+
+
+ setSchedule((s) => ({ ...s, smsDays: days }))}
+ />
+
+ Выбрано: {formatDays(schedule.smsDays) || "—"}
+
+
+
+ {/* Save */}
+
+
+ {hasChanges && !isSaving && (
+
+ )}
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ );
+};
\ No newline at end of file
diff --git a/src/components/client/PickupSlotsPicker.jsx b/src/components/client/PickupSlotsPicker.jsx
index 7690bb8..c5976b6 100644
--- a/src/components/client/PickupSlotsPicker.jsx
+++ b/src/components/client/PickupSlotsPicker.jsx
@@ -36,24 +36,27 @@ const getCrimeaHour = (referenceDate = new Date()) => {
);
};
-const isWeekend = (dateKey) => {
+const isAllowedPickupDay = (dateKey, allowedDays = [1,2,3,4,5]) => {
+ if (!dateKey) return false;
const d = new Date(`${dateKey}T12:00:00Z`);
- const day = d.getUTCDay();
- return day === 0 || day === 6;
+ // getUTCDay: 0=Sun, 1=Mon, ..., 6=Sat
+ // allowedDays uses 1=Mon ... 7=Sun
+ const dayNum = d.getUTCDay() === 0 ? 7 : d.getUTCDay();
+ return allowedDays.includes(dayNum);
};
-const getNextWorkday = (dateKey) => {
+const getNextPickupWorkday = (dateKey, allowedDays = [1,2,3,4,5]) => {
let next = addDaysKey(dateKey, 1);
- while (isWeekend(next)) {
+ while (!isAllowedPickupDay(next, allowedDays)) {
next = addDaysKey(next, 1);
}
return next;
};
-const getPickupSlots = (referenceDate = new Date()) => {
+const getPickupSlots = (referenceDate = new Date(), pickupDays = [1,2,3,4,5]) => {
const todayKey = getCrimeaTodayKey(referenceDate);
const hour = getCrimeaHour(referenceDate);
- const isTodayWorkday = !isWeekend(todayKey);
+ const isTodayWorkday = isAllowedPickupDay(todayKey, pickupDays);
const slots = [];
@@ -77,7 +80,7 @@ const getPickupSlots = (referenceDate = new Date()) => {
}
const tomorrow = addDaysKey(todayKey, 1);
- const tomorrowWorkday = !isWeekend(tomorrow) ? tomorrow : getNextWorkday(todayKey);
+ const tomorrowWorkday = isAllowedPickupDay(tomorrow, pickupDays) ? tomorrow : getNextPickupWorkday(todayKey, pickupDays);
slots.push({
id: `pickup-${tomorrowWorkday}-first`,
date: tomorrowWorkday,
@@ -94,7 +97,7 @@ const getPickupSlots = (referenceDate = new Date()) => {
});
const dayAfter = addDaysKey(tomorrowWorkday, 1);
- const dayAfterWorkday = !isWeekend(dayAfter) ? dayAfter : getNextWorkday(dayAfter);
+ const dayAfterWorkday = isAllowedPickupDay(dayAfter, pickupDays) ? dayAfter : getNextPickupWorkday(dayAfter, pickupDays);
slots.push({
id: `pickup-${dayAfterWorkday}-first`,
date: dayAfterWorkday,
@@ -152,8 +155,9 @@ export const PickupSlotsPicker = ({
onSelectSlot,
selectedSlotId,
referenceDate = new Date(),
+ pickupDays = [1, 2, 3, 4, 5],
}) => {
- const slots = React.useMemo(() => getPickupSlots(referenceDate), [referenceDate]);
+ const slots = React.useMemo(() => getPickupSlots(referenceDate, pickupDays), [referenceDate, pickupDays]);
if (!slots.length) {
return (
diff --git a/src/pages/ClientDeliveryPage.jsx b/src/pages/ClientDeliveryPage.jsx
index 6f4f4ee..00c8b77 100644
--- a/src/pages/ClientDeliveryPage.jsx
+++ b/src/pages/ClientDeliveryPage.jsx
@@ -14,6 +14,7 @@ import {
confirmDeliveryChoice,
fetchDeliveryInvitation,
} from "../services/deliveryInvitationApi";
+import { supabase } from "../supabaseClient";
const DELIVERY_TIMEZONE = "Europe/Simferopol";
@@ -42,15 +43,27 @@ const addDaysToDateKey = (dateKey, amount) => {
return baseDate.toISOString().slice(0, 10);
};
-const isSundayKey = (dateKey) => {
- if (!dateKey) return true;
- const d = new Date(`${dateKey}T12:00:00Z`);
- return d.getUTCDay() === 0;
+// Default: Mon-Fri (1-5). Overridden by business_schedule_settings from DB.
+let _deliveryDays = [1, 2, 3, 4, 5];
+let _pickupDays = [1, 2, 3, 4, 5];
+
+export const setScheduleDaysExternal = ({ deliveryDays, pickupDays }) => {
+ if (Array.isArray(deliveryDays) && deliveryDays.length) _deliveryDays = deliveryDays;
+ if (Array.isArray(pickupDays) && pickupDays.length) _pickupDays = pickupDays;
};
-const getNextWorkdayKey = (dateKey) => {
+const isAllowedDeliveryDay = (dateKey) => {
+ if (!dateKey) return false;
+ const d = new Date(`${dateKey}T12:00:00Z`);
+ // getUTCDay: 0=Sun, 1=Mon, ..., 6=Sat
+ // _deliveryDays uses 1=Mon ... 7=Sun
+ const dayNum = d.getUTCDay() === 0 ? 7 : d.getUTCDay();
+ return _deliveryDays.includes(dayNum);
+};
+
+const getNextDeliveryWorkdayKey = (dateKey) => {
let next = addDaysToDateKey(dateKey, 1);
- while (isSundayKey(next)) {
+ while (!isAllowedDeliveryDay(next)) {
next = addDaysToDateKey(next, 1);
}
return next;
@@ -58,8 +71,10 @@ const getNextWorkdayKey = (dateKey) => {
const getAllowedDeliveryDateKeys = (referenceDate = new Date()) => {
const todayKey = getBusinessTodayKey(referenceDate);
- const firstWorkday = getNextWorkdayKey(todayKey);
- const secondWorkday = getNextWorkdayKey(firstWorkday);
+ // If today is a delivery day, include it; otherwise start from next workday
+ const startKey = isAllowedDeliveryDay(todayKey) ? todayKey : getNextDeliveryWorkdayKey(todayKey);
+ const firstWorkday = isAllowedDeliveryDay(todayKey) ? todayKey : getNextDeliveryWorkdayKey(todayKey);
+ const secondWorkday = getNextDeliveryWorkdayKey(firstWorkday);
return new Set([firstWorkday, secondWorkday].filter(Boolean));
};
@@ -81,10 +96,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, skip non-delivery days)
const todayKey = getBusinessTodayKey(referenceDate);
- const firstWorkday = getNextWorkdayKey(todayKey);
- const secondWorkday = getNextWorkdayKey(firstWorkday);
+ const firstWorkday = isAllowedDeliveryDay(todayKey) ? todayKey : getNextDeliveryWorkdayKey(todayKey);
+ const secondWorkday = getNextDeliveryWorkdayKey(firstWorkday);
return [
{ id: `slot-${firstWorkday}-first`, date: firstWorkday, time: "Первая половина дня" },
{ id: `slot-${firstWorkday}-second`, date: firstWorkday, time: "Вторая половина дня" },
@@ -221,11 +236,35 @@ export const ClientDeliveryPage = () => {
const [choiceSaved, setChoiceSaved] = React.useState(false);
const [activeTab, setActiveTab] = React.useState(TAB_DELIVERY);
const [deliveryAddress, setDeliveryAddress] = React.useState("");
+ const [scheduleDays, setScheduleDays] = React.useState({ deliveryDays: [1,2,3,4,5], pickupDays: [1,2,3,4,5] });
const referenceDate = React.useMemo(
() => (invitation?.smsSentAt ? new Date(invitation.smsSentAt) : new Date()),
[token, invitation?.smsSentAt],
);
+ // Fetch business schedule (delivery/pickup days) — no auth required
+ React.useEffect(() => {
+ let cancelled = false;
+ const loadSchedule = async () => {
+ try {
+ const { data, error: rpcError } = await supabase.rpc("get_business_schedule");
+ if (rpcError) throw rpcError;
+ if (!cancelled && data?.ok) {
+ const days = {
+ deliveryDays: data.deliveryDays || [1,2,3,4,5],
+ pickupDays: data.pickupDays || [1,2,3,4,5],
+ };
+ setScheduleDays(days);
+ setScheduleDaysExternal(days);
+ }
+ } catch {
+ // Silent fallback — use defaults
+ }
+ };
+ loadSchedule();
+ return () => { cancelled = true; };
+ }, []);
+
React.useEffect(() => {
let cancelled = false;
@@ -505,6 +544,7 @@ export const ClientDeliveryPage = () => {
onSelectSlot={handleSlotSelect}
selectedSlotId={selectedSlotId}
referenceDate={referenceDate}
+ pickupDays={scheduleDays.pickupDays}
/>
) : null}
diff --git a/src/pages/DashboardPage.jsx b/src/pages/DashboardPage.jsx
index 990e665..974c5e3 100644
--- a/src/pages/DashboardPage.jsx
+++ b/src/pages/DashboardPage.jsx
@@ -17,6 +17,7 @@ import { StopWordsPanel } from "../components/admin/StopWordsPanel";
import { ActionLogPanel } from "../components/admin/ActionLogPanel";
import { SuggestionsPanel } from "../components/admin/SuggestionsPanel";
import { SmsCampaignPanel } from "../components/admin/SmsCampaignPanel";
+import { BusinessSchedulePanel } from "../components/admin/BusinessSchedulePanel";
import { Panel } from "../components/UI/Panel";
import { SkeletonPage, SkeletonTable } from "../components/UI/Loading";
import { useAuth } from "../context/AuthContext";
@@ -36,6 +37,7 @@ const MEGA_ADMIN_NAV = [
{ key: "action_log", label: "Журнал", description: "Журнал действий сотрудников.", badge: null },
{ key: "suggestions", label: "Предложения", description: "Предложения сотрудников по улучшению.", badge: null },
{ key: "sms_campaign", label: "SMS-кампании", description: "Логи и настройки SMS-рассылок.", badge: null },
+ { key: "schedule", label: "Расписание", description: "Рабочие дни доставки, самовывоза и SMS.", badge: null },
];
// ── Role → Default Section Map ─────────────────────────────────────────────
@@ -137,6 +139,7 @@ export const DashboardPage = () => {
{ key: "errors", label: "Ошибки", description: "Журнал ошибок приложения.", badge: null },
{ key: "action_log", label: "Журнал", description: "Журнал действий сотрудников.", badge: null },
{ key: "suggestions", label: "Предложения", description: "Предложения сотрудников по улучшению.", badge: null },
+ { key: "schedule", label: "Расписание", description: "Рабочие дни доставки, самовывоза и SMS.", badge: null },
]
: userRole === "logistician"
? [
@@ -177,6 +180,7 @@ const ALLOWED_DASHBOARD_ROLES = ["admin", "mega_admin", "manager", "logistician"
if (activeSection === "action_log") return ;
if (activeSection === "suggestions") return
;
if (activeSection === "sms_campaign") return
;
+ if (activeSection === "schedule") return
;
if (isLoading) {
if (userRole === "driver") {
diff --git a/supabase/functions/_shared/delivery-invitations.ts b/supabase/functions/_shared/delivery-invitations.ts
index a7fc938..9458ce3 100644
--- a/supabase/functions/_shared/delivery-invitations.ts
+++ b/supabase/functions/_shared/delivery-invitations.ts
@@ -119,9 +119,26 @@ export const normalizeAvailableSlots = (availableSlots?: string[] | null) => {
return slots.length > 0 ? Array.from(new Set(slots)) : [...DEFAULT_AVAILABLE_SLOTS];
};
-export const buildDefaultDatedAvailableSlots = (now = new Date()) => {
+export const buildDefaultDatedAvailableSlots = async (now = new Date(), supabaseClient?: any) => {
const CRIMEA_TZ = "Europe/Simferopol";
+ // Fetch delivery days from business_schedule_settings
+ let deliveryDays: number[] = [1, 2, 3, 4, 5]; // Default: Mon-Fri
+ if (supabaseClient) {
+ try {
+ const { data } = await supabaseClient
+ .from("business_schedule_settings")
+ .select("delivery_days")
+ .eq("id", 1)
+ .single();
+ if (data?.delivery_days && Array.isArray(data.delivery_days) && data.delivery_days.length) {
+ deliveryDays = data.delivery_days;
+ }
+ } catch {
+ // Silent fallback to defaults
+ }
+ }
+
const formatCrimeaDate = (date: Date) => {
return new Intl.DateTimeFormat("en-CA", {
timeZone: CRIMEA_TZ,
@@ -137,12 +154,18 @@ export const buildDefaultDatedAvailableSlots = (now = new Date()) => {
return next;
};
- // Skip Sunday (getUTCDay() === 0) — never offer Sunday delivery
- const isSunday = (date: Date) => date.getUTCDay() === 0;
+ // Check if date is an allowed delivery day
+ // getUTCDay: 0=Sun, 1=Mon, ..., 6=Sat
+ // deliveryDays uses 1=Mon ... 7=Sun
+ const isAllowedDay = (date: Date) => {
+ const dow = date.getUTCDay();
+ const dayNum = dow === 0 ? 7 : dow;
+ return deliveryDays.includes(dayNum);
+ };
const getNextWorkday = (date: Date) => {
let next = addDays(date, 1);
- while (isSunday(next)) {
+ while (!isAllowedDay(next)) {
next = addDays(next, 1);
}
return next;
diff --git a/supabase/functions/confirm-delivery-choice/index.ts b/supabase/functions/confirm-delivery-choice/index.ts
index 33b29eb..65a5d78 100644
--- a/supabase/functions/confirm-delivery-choice/index.ts
+++ b/supabase/functions/confirm-delivery-choice/index.ts
@@ -31,11 +31,31 @@ type ConfirmBody = {
const isValidDate = (value: string) => /^\d{4}-\d{2}-\d{2}$/.test(value);
-const isWeekendDate = (value: string) => {
+// Fetch delivery days from business_schedule_settings
+const getDeliveryDays = async (supabaseClient: any): Promise => {
+ try {
+ const { data } = await supabaseClient
+ .from("business_schedule_settings")
+ .select("delivery_days")
+ .eq("id", 1)
+ .single();
+ if (data?.delivery_days && Array.isArray(data.delivery_days) && data.delivery_days.length) {
+ return data.delivery_days as number[];
+ }
+ } catch {
+ // Silent fallback
+ }
+ return [1, 2, 3, 4, 5]; // Default: Mon-Fri
+};
+
+const isAllowedDeliveryDate = (value: string, deliveryDays: number[]) => {
if (!isValidDate(value)) return false;
const date = new Date(`${value}T12:00:00Z`);
- const weekday = date.getUTCDay();
- return weekday === 0; // 0=Sunday — never allow Sunday delivery
+ const dow = date.getUTCDay();
+ // getUTCDay: 0=Sun, 1=Mon, ..., 6=Sat
+ // deliveryDays uses 1=Mon ... 7=Sun
+ const dayNum = dow === 0 ? 7 : dow;
+ return deliveryDays.includes(dayNum);
};
const resolveRequestedSlot = (
@@ -45,6 +65,7 @@ const resolveRequestedSlot = (
available_slots?: string[] | null;
},
body: ConfirmBody,
+ deliveryDays: number[] = [1, 2, 3, 4, 5],
) => {
const deliveryType = body.deliveryType || "delivery";
const deliveryDate = String(body.deliveryDate || invitation.delivery_date || "").trim();
@@ -59,8 +80,8 @@ const resolveRequestedSlot = (
return { deliveryDate, deliveryTime, deliveryType };
}
- // Reject Sunday for delivery (business rule: never deliver on Sunday)
- if (isWeekendDate(deliveryDate)) {
+ // Reject non-delivery days for delivery (business schedule)
+ if (!isAllowedDeliveryDate(deliveryDate, deliveryDays)) {
return null;
}
@@ -120,6 +141,7 @@ Deno.serve(async (request) => {
const tokenHash = await hashInvitationToken(body.token);
const supabase = createServiceClient();
+ const deliveryDays = await getDeliveryDays(supabase);
const ipHash = await hashText(getClientIp(request));
await requireRateLimit(supabase, {
@@ -180,7 +202,7 @@ Deno.serve(async (request) => {
);
}
- const requestedSlot = resolveRequestedSlot(invitation, body);
+ const requestedSlot = resolveRequestedSlot(invitation, body, deliveryDays);
if (!requestedSlot) {
return jsonResponse(
{
diff --git a/volumes/functions/_shared/delivery-invitations.ts b/volumes/functions/_shared/delivery-invitations.ts
index 70e7ade..9458ce3 100644
--- a/volumes/functions/_shared/delivery-invitations.ts
+++ b/volumes/functions/_shared/delivery-invitations.ts
@@ -119,16 +119,63 @@ export const normalizeAvailableSlots = (availableSlots?: string[] | null) => {
return slots.length > 0 ? Array.from(new Set(slots)) : [...DEFAULT_AVAILABLE_SLOTS];
};
-export const buildDefaultDatedAvailableSlots = (now = new Date()) => {
- const formatIsoDate = (date: Date) => date.toISOString().slice(0, 10);
+export const buildDefaultDatedAvailableSlots = async (now = new Date(), supabaseClient?: any) => {
+ const CRIMEA_TZ = "Europe/Simferopol";
+
+ // Fetch delivery days from business_schedule_settings
+ let deliveryDays: number[] = [1, 2, 3, 4, 5]; // Default: Mon-Fri
+ if (supabaseClient) {
+ try {
+ const { data } = await supabaseClient
+ .from("business_schedule_settings")
+ .select("delivery_days")
+ .eq("id", 1)
+ .single();
+ if (data?.delivery_days && Array.isArray(data.delivery_days) && data.delivery_days.length) {
+ deliveryDays = data.delivery_days;
+ }
+ } catch {
+ // Silent fallback to defaults
+ }
+ }
+
+ const formatCrimeaDate = (date: Date) => {
+ return new Intl.DateTimeFormat("en-CA", {
+ timeZone: CRIMEA_TZ,
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ }).format(date);
+ };
+
const addDays = (date: Date, days: number) => {
const next = new Date(date);
next.setUTCDate(next.getUTCDate() + days);
return next;
};
- const firstDay = formatIsoDate(addDays(now, 1));
- const secondDay = formatIsoDate(addDays(now, 2));
+ // Check if date is an allowed delivery day
+ // getUTCDay: 0=Sun, 1=Mon, ..., 6=Sat
+ // deliveryDays uses 1=Mon ... 7=Sun
+ const isAllowedDay = (date: Date) => {
+ const dow = date.getUTCDay();
+ const dayNum = dow === 0 ? 7 : dow;
+ return deliveryDays.includes(dayNum);
+ };
+
+ const getNextWorkday = (date: Date) => {
+ let next = addDays(date, 1);
+ while (!isAllowedDay(next)) {
+ next = addDays(next, 1);
+ }
+ return next;
+ };
+
+ const firstWorkday = getNextWorkday(now);
+ const secondWorkday = getNextWorkday(firstWorkday);
+
+ const firstDay = formatCrimeaDate(firstWorkday);
+ const secondDay = formatCrimeaDate(secondWorkday);
return [
`${firstDay}, Первая половина дня`,
diff --git a/volumes/functions/confirm-delivery-choice/index.ts b/volumes/functions/confirm-delivery-choice/index.ts
index 33b29eb..65a5d78 100644
--- a/volumes/functions/confirm-delivery-choice/index.ts
+++ b/volumes/functions/confirm-delivery-choice/index.ts
@@ -31,11 +31,31 @@ type ConfirmBody = {
const isValidDate = (value: string) => /^\d{4}-\d{2}-\d{2}$/.test(value);
-const isWeekendDate = (value: string) => {
+// Fetch delivery days from business_schedule_settings
+const getDeliveryDays = async (supabaseClient: any): Promise => {
+ try {
+ const { data } = await supabaseClient
+ .from("business_schedule_settings")
+ .select("delivery_days")
+ .eq("id", 1)
+ .single();
+ if (data?.delivery_days && Array.isArray(data.delivery_days) && data.delivery_days.length) {
+ return data.delivery_days as number[];
+ }
+ } catch {
+ // Silent fallback
+ }
+ return [1, 2, 3, 4, 5]; // Default: Mon-Fri
+};
+
+const isAllowedDeliveryDate = (value: string, deliveryDays: number[]) => {
if (!isValidDate(value)) return false;
const date = new Date(`${value}T12:00:00Z`);
- const weekday = date.getUTCDay();
- return weekday === 0; // 0=Sunday — never allow Sunday delivery
+ const dow = date.getUTCDay();
+ // getUTCDay: 0=Sun, 1=Mon, ..., 6=Sat
+ // deliveryDays uses 1=Mon ... 7=Sun
+ const dayNum = dow === 0 ? 7 : dow;
+ return deliveryDays.includes(dayNum);
};
const resolveRequestedSlot = (
@@ -45,6 +65,7 @@ const resolveRequestedSlot = (
available_slots?: string[] | null;
},
body: ConfirmBody,
+ deliveryDays: number[] = [1, 2, 3, 4, 5],
) => {
const deliveryType = body.deliveryType || "delivery";
const deliveryDate = String(body.deliveryDate || invitation.delivery_date || "").trim();
@@ -59,8 +80,8 @@ const resolveRequestedSlot = (
return { deliveryDate, deliveryTime, deliveryType };
}
- // Reject Sunday for delivery (business rule: never deliver on Sunday)
- if (isWeekendDate(deliveryDate)) {
+ // Reject non-delivery days for delivery (business schedule)
+ if (!isAllowedDeliveryDate(deliveryDate, deliveryDays)) {
return null;
}
@@ -120,6 +141,7 @@ Deno.serve(async (request) => {
const tokenHash = await hashInvitationToken(body.token);
const supabase = createServiceClient();
+ const deliveryDays = await getDeliveryDays(supabase);
const ipHash = await hashText(getClientIp(request));
await requireRateLimit(supabase, {
@@ -180,7 +202,7 @@ Deno.serve(async (request) => {
);
}
- const requestedSlot = resolveRequestedSlot(invitation, body);
+ const requestedSlot = resolveRequestedSlot(invitation, body, deliveryDays);
if (!requestedSlot) {
return jsonResponse(
{