From 663b31b3df1ad55c3a717644378218a1cbc1b730 Mon Sep 17 00:00:00 2001 From: Hermes Date: Thu, 13 Aug 2026 14:17:18 +0200 Subject: [PATCH] feat(client): test consent page + 'test notification' button on /client/test --- src/pages/ClientTestConsentPage.jsx | 204 ++++++++++++++++++++++++++++ src/pages/ClientTestPage.jsx | 71 +++++++++- src/router.jsx | 5 + 3 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 src/pages/ClientTestConsentPage.jsx diff --git a/src/pages/ClientTestConsentPage.jsx b/src/pages/ClientTestConsentPage.jsx new file mode 100644 index 0000000..c05c874 --- /dev/null +++ b/src/pages/ClientTestConsentPage.jsx @@ -0,0 +1,204 @@ +import React, { useState } from "react"; +import { Panel } from "../components/UI/Panel"; +import { Button } from "../components/UI/Button"; +import { formatDeliveryDate, getDeliveryRelativeDayLabel } from "../components/client/deliveryDateFormatting"; + +const TAB_DELIVERY = "delivery"; +const TAB_PICKUP = "pickup"; + +const MOCK_PRODUCTS = [ + { name: "Цемент М500, 50 кг", quantity: "10", unit: "меш" }, + { name: "Песок строительный", quantity: "2", unit: "м³" }, + { name: "Кирпич рядовой", quantity: "400", unit: "шт" }, + { name: "Арматура 12 мм", quantity: "60", unit: "м" }, +]; + +const buildMockSlots = () => { + const day = (offset) => { + const d = new Date(); + d.setDate(d.getDate() + offset); + return d.toISOString().slice(0, 10); + }; + + const skipSunday = (dateStr) => { + const d = new Date(`${dateStr}T12:00:00Z`); + if (d.getUTCDay() === 0) { + d.setUTCDate(d.getUTCDate() + 1); + } + return d.toISOString().slice(0, 10); + }; + + const d1 = skipSunday(day(1)); + const d2 = skipSunday(day(2)); + + return [ + { id: `slot-${d1}-first`, date: d1, time: "Первая половина дня" }, + { id: `slot-${d1}-second`, date: d1, time: "Вторая половина дня" }, + { id: `slot-${d2}-first`, date: d2, time: "Первая половина дня" }, + { id: `slot-${d2}-second`, date: d2, time: "Вторая половина дня" }, + ]; +}; + +const groupSlotsByDate = (slots) => { + const groups = new Map(); + for (const slot of slots) { + if (!groups.has(slot.date)) groups.set(slot.date, []); + groups.get(slot.date).push(slot); + } + return Array.from(groups.entries()).sort(([a], [b]) => a.localeCompare(b)); +}; + +const SlotGroupHeading = ({ dateStr }) => { + const relative = getDeliveryRelativeDayLabel(dateStr); + const formatted = formatDeliveryDate(dateStr); + return relative + ? `Доставка ${relative.charAt(0).toLowerCase()}${relative.slice(1)} · ${formatted}` + : `Доставка ${formatted}`; +}; + +export const ClientTestConsentPage = () => { + const [activeTab, setActiveTab] = useState(TAB_DELIVERY); + const [selectedSlot, setSelectedSlot] = useState(null); + const [saved, setSaved] = useState(false); + + const slots = buildMockSlots(); + const grouped = groupSlotsByDate(slots); + const typeLabel = activeTab === TAB_PICKUP ? "самовывоз" : "доставку"; + + const handleSave = () => { + if (!selectedSlot) return; + setSaved(true); + }; + + return ( +
+
+ + +

+ Тестовая страница · согласование +

+

+ {activeTab === TAB_PICKUP ? "Выберите время самовывоза" : "Выберите время доставки"} +

+

+ Это тестовая страница — данные ненастоящие. Выберите удобную половину дня для {typeLabel}. +

+
+ + +
+ +
+

Заказ TEST-001 · счёт 1

+

Тестов Тест Тестович

+

+ {activeTab === TAB_PICKUP + ? "Самовывоз со склада" + : "г. Симферополь, ул. Тестовая, д. 1"} +

+
+
+ +
+ + +
+
+ + +

Состав заказа

+
+ {MOCK_PRODUCTS.map((p, idx) => ( +
+ {p.name} + + {p.quantity} {p.unit} + +
+ ))} +
+
+ + {saved && selectedSlot ? ( + +

Ваш выбор

+

+ {activeTab === TAB_PICKUP ? "Самовывоз" : "Доставка"}: {formatDeliveryDate(selectedSlot.date)} / {selectedSlot.time} +

+

+ Выбор сохранен (тестовая страница — в реальном заказе здесь будет подтверждение от менеджера). +

+
+ ) : ( + <> + {grouped.map(([date, dateSlots]) => ( +
+ +
+

+ Раскрыть + Свернуть +
+
+
+
+ {dateSlots.map((slot) => { + const isSelected = selectedSlot?.id === slot.id; + return ( + + ); + })} +
+
+
+ ))} + +
+ +
+ + )} +
+
+ ); +}; + +export default ClientTestConsentPage; diff --git a/src/pages/ClientTestPage.jsx b/src/pages/ClientTestPage.jsx index 3b58e1a..2f46e7c 100644 --- a/src/pages/ClientTestPage.jsx +++ b/src/pages/ClientTestPage.jsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import { Panel } from "../components/UI/Panel"; import { PwaInstallCard } from "../components/client/PwaInstallCard"; import { PushSubscriptionBanner } from "../components/client/PushSubscriptionBanner"; @@ -8,6 +8,10 @@ import { PushSubscriptionBanner } from "../components/client/PushSubscriptionBan * Mock data — no real order, no API calls. * Route: /client/test */ + +const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL || ""; +const SUPABASE_ANON_KEY = import.meta.env.VITE_SUPABASE_ANON_KEY || ""; + const MOCK_INVITATION = { customerName: "Тестов Тест Тестович", customerPhone: "79990000000", @@ -20,7 +24,42 @@ const MOCK_INVITATION = { orderGroupId: "test-group-001", }; +const sendTestPush = async (phone) => { + const resp = await fetch(`${SUPABASE_URL}/functions/v1/send-test-push`, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(SUPABASE_ANON_KEY ? { apikey: SUPABASE_ANON_KEY } : {}), + }, + body: JSON.stringify({ + phone_normalized: phone, + title: "СуперСам — согласуйте доставку", + body: "Ваш заказ готов к доставке. Выберите удобное время.", + url: `${window.location.origin}/client/test-consent`, + }), + }); + const data = await resp.json().catch(() => ({})); + return { ok: resp.ok, ...data }; +}; + export const ClientTestPage = () => { + const [sending, setSending] = useState(false); + const [sendResult, setSendResult] = useState(null); // null | {ok, sent, error} + + const handleTestPush = async () => { + setSending(true); + setSendResult(null); + try { + const phone = window.localStorage.getItem("supersam_phone") || MOCK_INVITATION.customerPhone; + const result = await sendTestPush(phone); + setSendResult(result); + } catch (e) { + setSendResult({ ok: false, error: e instanceof Error ? e.message : "Ошибка отправки" }); + } finally { + setSending(false); + } + }; + return (
@@ -60,6 +99,36 @@ export const ClientTestPage = () => { orderGroupId={MOCK_INVITATION.orderGroupId} /> + +
+

🧪 Тест уведомления

+

+ Отправить тестовое push-уведомление на этот телефон. Нажмите на уведомление — + откроется тестовая страница согласования доставки. +

+
+ + + {sendResult?.ok ? ( +

+ ✅ Отправлено: {sendResult.sent} уведомление(й). Проверьте телефон. +

+ ) : null} + + {sendResult && !sendResult.ok ? ( +

+ ❌ {sendResult.error || "Не удалось отправить уведомление"} +

+ ) : null} +
+
); diff --git a/src/router.jsx b/src/router.jsx index ece7aea..f8195f1 100644 --- a/src/router.jsx +++ b/src/router.jsx @@ -3,6 +3,7 @@ import { Navigate, createBrowserRouter, useParams } from "react-router-dom"; import App from "./App"; import { ClientDeliveryPage } from "./pages/ClientDeliveryPage"; import { ClientTestPage } from "./pages/ClientTestPage"; +import { ClientTestConsentPage } from "./pages/ClientTestConsentPage"; import { DashboardPage } from "./pages/DashboardPage"; import { GroupDetailPage } from "./pages/GroupDetailPage"; import { LoginPage } from "./pages/LoginPage"; @@ -56,6 +57,10 @@ export const router = createBrowserRouter([ path: "client/test", element: , }, + { + path: "client/test-consent", + element: , + }, { path: "delivery/:token", element: ,